dashboard.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584
  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. todayRevenue: signal(28500)
  23. };
  24. // 紧急待办列表
  25. urgentTasks = signal<Task[]>([]);
  26. // 任务处理状态
  27. taskProcessingState = signal<{[key: string]: {inProgress: boolean, progress: number}}>({});
  28. // 项目动态流
  29. projectUpdates = signal<(Project | CustomerFeedback)[]>([]);
  30. // 搜索关键词
  31. searchTerm = signal('');
  32. // 筛选后的项目更新
  33. filteredUpdates = computed(() => {
  34. if (!this.searchTerm()) return this.projectUpdates();
  35. return this.projectUpdates().filter(item => {
  36. if ('name' in item) {
  37. // 项目
  38. return item.name.toLowerCase().includes(this.searchTerm().toLowerCase()) ||
  39. item.customerName.toLowerCase().includes(this.searchTerm().toLowerCase()) ||
  40. item.status.toLowerCase().includes(this.searchTerm().toLowerCase());
  41. } else {
  42. // 反馈
  43. return 'content' in item && item.content.toLowerCase().includes(this.searchTerm().toLowerCase()) ||
  44. 'status' in item && item.status.toLowerCase().includes(this.searchTerm().toLowerCase());
  45. }
  46. });
  47. });
  48. currentDate = new Date();
  49. // 回到顶部按钮可见性信号
  50. showBackToTopSignal = signal(false);
  51. // 任务表单可见性
  52. isTaskFormVisible = signal(false);
  53. // 新任务数据
  54. newTask: Task = {
  55. id: '',
  56. projectId: '',
  57. projectName: '',
  58. title: '',
  59. stage: '需求沟通',
  60. deadline: new Date(),
  61. isOverdue: false,
  62. isCompleted: false,
  63. priority: 'high',
  64. assignee: '当前用户',
  65. description: ''
  66. };
  67. // 用于日期时间输入的属性
  68. deadlineInput = '';
  69. // 预设快捷时长选项
  70. timePresets = [
  71. { label: '1小时内', hours: 1 },
  72. { label: '3小时内', hours: 3 },
  73. { label: '6小时内', hours: 6 },
  74. { label: '12小时内', hours: 12 },
  75. { label: '24小时内', hours: 24 }
  76. ];
  77. // 选中的预设时长
  78. selectedPreset = '';
  79. // 自定义时间弹窗可见性
  80. isCustomTimeVisible = false;
  81. // 自定义选择的日期和时间
  82. customDate = new Date();
  83. customTime = '';
  84. // 错误提示信息
  85. deadlineError = '';
  86. // 提交按钮是否禁用
  87. isSubmitDisabled = false;
  88. // 下拉框可见性
  89. deadlineDropdownVisible = false;
  90. // 日期范围限制
  91. get todayDate(): string {
  92. return new Date().toISOString().split('T')[0];
  93. }
  94. get sevenDaysLaterDate(): string {
  95. const date = new Date();
  96. date.setDate(date.getDate() + 7);
  97. return date.toISOString().split('T')[0];
  98. }
  99. constructor(
  100. private projectService: ProjectService,
  101. private router: Router,
  102. private activatedRoute: ActivatedRoute
  103. ) {}
  104. ngOnInit(): void {
  105. this.loadUrgentTasks();
  106. this.loadProjectUpdates();
  107. // 添加滚动事件监听
  108. window.addEventListener('scroll', this.onScroll.bind(this));
  109. }
  110. // 添加滚动事件处理方法
  111. private onScroll(): void {
  112. this.showBackToTopSignal.set(window.scrollY > 300);
  113. }
  114. // 添加显示回到顶部按钮的计算属性
  115. showBackToTop = computed(() => this.showBackToTopSignal());
  116. // 清理事件监听器
  117. ngOnDestroy(): void {
  118. window.removeEventListener('scroll', this.onScroll.bind(this));
  119. }
  120. // 添加scrollToTop方法
  121. scrollToTop(): void {
  122. window.scrollTo({
  123. top: 0,
  124. behavior: 'smooth'
  125. });
  126. }
  127. // 修改loadUrgentTasks方法,添加status属性
  128. loadUrgentTasks(): void {
  129. // 从服务获取任务数据,筛选出紧急任务
  130. this.projectService.getTasks().subscribe(tasks => {
  131. const filteredTasks = tasks.map(task => ({...task, status: task.isOverdue ? '已逾期' : task.isCompleted ? '已完成' : '进行中'}))
  132. .filter(task => task.isOverdue || task.deadline.toDateString() === new Date().toDateString());
  133. this.urgentTasks.set(filteredTasks.sort((a, b) => {
  134. // 按紧急程度排序
  135. if (a.isOverdue && !b.isOverdue) return -1;
  136. if (!a.isOverdue && b.isOverdue) return 1;
  137. return a.deadline.getTime() - b.deadline.getTime();
  138. }));
  139. });
  140. }
  141. loadProjectUpdates(): void {
  142. // 模拟项目更新数据
  143. this.projectService.getProjects().subscribe(projects => {
  144. this.projectService.getCustomerFeedbacks().subscribe(feedbacks => {
  145. // 合并项目和反馈,按时间倒序排序
  146. const updates: (Project | CustomerFeedback)[] = [
  147. ...projects,
  148. ...feedbacks
  149. ].sort((a, b) => {
  150. const dateA = 'createdAt' in a ? a.createdAt : new Date(a['updatedAt'] || a['deadline']);
  151. const dateB = 'createdAt' in b ? b.createdAt : new Date(b['updatedAt'] || b['deadline']);
  152. return dateB.getTime() - dateA.getTime();
  153. }).slice(0, 20); // 限制显示20条
  154. this.projectUpdates.set(updates);
  155. });
  156. });
  157. }
  158. // 处理任务完成
  159. markTaskAsCompleted(taskId: string): void {
  160. this.urgentTasks.set(
  161. this.urgentTasks().map(task =>
  162. task.id === taskId ? { ...task, isCompleted: true, status: '已完成' } : task
  163. )
  164. );
  165. }
  166. // 处理派单操作
  167. handleAssignment(taskId: string): void {
  168. // 标记任务为处理中
  169. const task = this.urgentTasks().find(t => t.id === taskId);
  170. if (task) {
  171. // 初始化处理状态
  172. this.taskProcessingState.update(state => ({
  173. ...state,
  174. [task.id]: { inProgress: true, progress: 0 }
  175. }));
  176. // 模拟处理进度
  177. let progress = 0;
  178. const interval = setInterval(() => {
  179. progress += 10;
  180. this.taskProcessingState.update(state => ({
  181. ...state,
  182. [task.id]: { inProgress: progress < 100, progress }
  183. }));
  184. if (progress >= 100) {
  185. clearInterval(interval);
  186. // 处理完成后从列表中移除该任务
  187. this.urgentTasks.set(
  188. this.urgentTasks().filter(t => t.id !== task.id)
  189. );
  190. // 清除处理状态
  191. this.taskProcessingState.update(state => {
  192. const newState = { ...state };
  193. delete newState[task.id];
  194. return newState;
  195. });
  196. }
  197. }, 300);
  198. }
  199. // 更新统计数据
  200. this.stats.pendingAssignments.set(this.stats.pendingAssignments() - 1);
  201. }
  202. // 显示任务表单
  203. showTaskForm(): void {
  204. // 重置表单数据
  205. this.newTask = {
  206. id: '',
  207. projectId: '',
  208. projectName: '',
  209. title: '',
  210. stage: '需求沟通',
  211. deadline: new Date(),
  212. isOverdue: false,
  213. isCompleted: false,
  214. priority: 'high',
  215. assignee: '当前用户',
  216. description: ''
  217. };
  218. // 重置相关状态
  219. this.deadlineError = '';
  220. this.isSubmitDisabled = false;
  221. // 计算并设置默认预设时长
  222. this.setDefaultPreset();
  223. // 显示表单
  224. this.isTaskFormVisible.set(true);
  225. // 添加iOS风格的面板显示动画
  226. setTimeout(() => {
  227. document.querySelector('.ios-panel')?.classList.add('ios-panel-visible');
  228. }, 10);
  229. }
  230. // 设置默认预设时长
  231. private setDefaultPreset(): void {
  232. const now = new Date();
  233. const todayEnd = new Date(now);
  234. todayEnd.setHours(23, 59, 59, 999);
  235. // 检查3小时后是否超过当天24:00
  236. const threeHoursLater = new Date(now.getTime() + 3 * 60 * 60 * 1000);
  237. if (threeHoursLater <= todayEnd) {
  238. // 3小时后未超过当天24:00,默认选中3小时内
  239. this.selectedPreset = '3';
  240. this.updatePresetDeadline(3);
  241. } else {
  242. // 3小时后超过当天24:00,默认选中当天24:00前
  243. this.selectedPreset = 'today';
  244. this.deadlineInput = todayEnd.toISOString().slice(0, 16);
  245. this.newTask.deadline = todayEnd;
  246. }
  247. }
  248. // 处理预设时长选择
  249. handlePresetSelection(preset: string): void {
  250. this.selectedPreset = preset;
  251. this.deadlineError = '';
  252. if (preset === 'custom') {
  253. // 打开自定义时间选择器
  254. this.openCustomTimePicker();
  255. } else if (preset === 'today') {
  256. // 设置为当天24:00前
  257. const now = new Date();
  258. const todayEnd = new Date(now);
  259. todayEnd.setHours(23, 59, 59, 999);
  260. this.deadlineInput = todayEnd.toISOString().slice(0, 16);
  261. this.newTask.deadline = todayEnd;
  262. } else {
  263. // 计算预设时长的截止时间
  264. const hours = parseInt(preset);
  265. this.updatePresetDeadline(hours);
  266. }
  267. }
  268. // 更新预设时长的截止时间
  269. private updatePresetDeadline(hours: number): void {
  270. const now = new Date();
  271. const deadline = new Date(now.getTime() + hours * 60 * 60 * 1000);
  272. this.deadlineInput = deadline.toISOString().slice(0, 16);
  273. this.newTask.deadline = deadline;
  274. }
  275. // 打开自定义时间选择器
  276. openCustomTimePicker(): void {
  277. // 重置自定义时间
  278. this.customDate = new Date();
  279. const now = new Date();
  280. this.customTime = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
  281. // 显示自定义时间弹窗
  282. this.isCustomTimeVisible = true;
  283. // 添加iOS风格的弹窗动画
  284. setTimeout(() => {
  285. document.querySelector('.custom-time-modal')?.classList.add('modal-visible');
  286. }, 10);
  287. }
  288. // 关闭自定义时间选择器
  289. closeCustomTimePicker(): void {
  290. // 添加iOS风格的弹窗关闭动画
  291. const modal = document.querySelector('.custom-time-modal');
  292. if (modal) {
  293. modal.classList.remove('modal-visible');
  294. setTimeout(() => {
  295. this.isCustomTimeVisible = false;
  296. }, 300);
  297. } else {
  298. this.isCustomTimeVisible = false;
  299. }
  300. }
  301. // 处理自定义时间选择
  302. handleCustomTimeSelection(): void {
  303. const [hours, minutes] = this.customTime.split(':').map(Number);
  304. const selectedDateTime = new Date(this.customDate);
  305. selectedDateTime.setHours(hours, minutes, 0, 0);
  306. // 验证选择的时间是否有效
  307. if (this.validateDeadline(selectedDateTime)) {
  308. this.deadlineInput = selectedDateTime.toISOString().slice(0, 16);
  309. this.newTask.deadline = selectedDateTime;
  310. this.closeCustomTimePicker();
  311. }
  312. }
  313. // 验证截止时间是否有效
  314. validateDeadline(deadline: Date): boolean {
  315. const now = new Date();
  316. if (deadline < now) {
  317. this.deadlineError = '截止时间不能早于当前时间,请重新选择';
  318. this.isSubmitDisabled = true;
  319. return false;
  320. }
  321. this.deadlineError = '';
  322. this.isSubmitDisabled = false;
  323. return true;
  324. }
  325. // 获取显示的截止时间文本
  326. getDisplayDeadline(): string {
  327. if (!this.deadlineInput) return '';
  328. try {
  329. const date = new Date(this.deadlineInput);
  330. return date.toLocaleString('zh-CN', {
  331. year: 'numeric',
  332. month: '2-digit',
  333. day: '2-digit',
  334. hour: '2-digit',
  335. minute: '2-digit'
  336. });
  337. } catch (error) {
  338. return '';
  339. }
  340. }
  341. // 隐藏任务表单
  342. hideTaskForm(): void {
  343. // 添加iOS风格的面板隐藏动画
  344. const panel = document.querySelector('.ios-panel');
  345. if (panel) {
  346. panel.classList.remove('ios-panel-visible');
  347. setTimeout(() => {
  348. this.isTaskFormVisible.set(false);
  349. }, 300);
  350. } else {
  351. this.isTaskFormVisible.set(false);
  352. }
  353. }
  354. // 处理添加任务表单提交
  355. handleAddTaskSubmit(): void {
  356. // 验证表单数据
  357. if (!this.newTask.title.trim() || !this.newTask.projectName.trim() || !this.deadlineInput || this.isSubmitDisabled) {
  358. // 在实际应用中,这里应该显示错误提示
  359. alert('请填写必填字段(任务标题、项目名称、截止时间)');
  360. return;
  361. }
  362. // 创建新任务
  363. const taskToAdd: Task = {
  364. ...this.newTask,
  365. id: `task-${Date.now()}`,
  366. projectId: `project-${Math.floor(Math.random() * 1000)}`,
  367. deadline: new Date(this.deadlineInput),
  368. isOverdue: new Date(this.deadlineInput) < new Date()
  369. };
  370. // 添加到任务列表
  371. this.urgentTasks.set([taskToAdd, ...this.urgentTasks()]);
  372. // 更新统计数据
  373. this.stats.pendingAssignments.set(this.stats.pendingAssignments() + 1);
  374. // 隐藏表单
  375. this.hideTaskForm();
  376. }
  377. // 添加新的紧急事项
  378. addUrgentTask(): void {
  379. // 调用显示表单方法
  380. this.showTaskForm();
  381. }
  382. // 新咨询数图标点击处理
  383. handleNewConsultationsClick(): void {
  384. this.navigateToDetail('consultations');
  385. }
  386. // 待派单数图标点击处理
  387. handlePendingAssignmentsClick(): void {
  388. this.navigateToDetail('assignments');
  389. }
  390. // 异常项目图标点击处理
  391. handleExceptionProjectsClick(): void {
  392. this.navigateToDetail('exceptions');
  393. }
  394. // 今日成交额图标点击处理
  395. handleTodayRevenueClick(): void {
  396. this.navigateToDetail('revenue');
  397. }
  398. // 导航到详情页
  399. private navigateToDetail(type: 'consultations' | 'assignments' | 'exceptions' | 'revenue'): void {
  400. const routeMap = {
  401. consultations: '/customer-service/consultation-list',
  402. assignments: '/customer-service/assignment-list',
  403. exceptions: '/customer-service/exception-list',
  404. revenue: '/customer-service/revenue-detail'
  405. };
  406. console.log('导航到:', routeMap[type]);
  407. console.log('当前路由:', this.router.url);
  408. // 添加iOS风格页面过渡动画
  409. document.body.classList.add('ios-page-transition');
  410. setTimeout(() => {
  411. this.router.navigateByUrl(routeMap[type])
  412. .then(navResult => {
  413. console.log('导航结果:', navResult);
  414. if (!navResult) {
  415. console.error('导航失败,检查路由配置');
  416. }
  417. })
  418. .catch(err => {
  419. console.error('导航错误:', err);
  420. });
  421. setTimeout(() => {
  422. document.body.classList.remove('ios-page-transition');
  423. }, 300);
  424. }, 100);
  425. }
  426. // 格式化日期
  427. formatDate(date: Date | string): string {
  428. if (!date) return '';
  429. try {
  430. return new Date(date).toLocaleString('zh-CN', {
  431. month: '2-digit',
  432. day: '2-digit',
  433. hour: '2-digit',
  434. minute: '2-digit'
  435. });
  436. } catch (error) {
  437. console.error('日期格式化错误:', error);
  438. return '';
  439. }
  440. }
  441. // 添加安全获取客户名称的方法
  442. getCustomerName(update: Project | CustomerFeedback): string {
  443. if ('customerName' in update && update.customerName) {
  444. return update.customerName;
  445. } else if ('projectId' in update) {
  446. // 查找相关项目获取客户名称
  447. return '客户反馈';
  448. }
  449. return '未知客户';
  450. }
  451. // 优化的日期格式化方法
  452. getFormattedDate(update: Project | CustomerFeedback): string {
  453. if (!update) return '';
  454. if ('createdAt' in update && update.createdAt) {
  455. return this.formatDate(update.createdAt);
  456. } else if ('updatedAt' in update && update.updatedAt) {
  457. return this.formatDate(update.updatedAt);
  458. } else if ('deadline' in update && update.deadline) {
  459. return this.formatDate(update.deadline);
  460. }
  461. return '';
  462. }
  463. // 添加获取状态的安全方法
  464. getUpdateStatus(update: Project | CustomerFeedback): string {
  465. if ('status' in update && update.status) {
  466. return update.status;
  467. }
  468. return '已更新';
  469. }
  470. // 添加getTaskStatus方法的正确实现
  471. getTaskStatus(task: Task): string {
  472. if (!task) return '未知状态';
  473. if (task.isCompleted) return '已完成';
  474. if (task.isOverdue) return '已逾期';
  475. return '进行中';
  476. }
  477. // 添加getUpdateStatusClass方法的正确实现
  478. getUpdateStatusClass(update: Project | CustomerFeedback): string {
  479. if (!update || !('status' in update) || !update.status) return '';
  480. switch (update.status) {
  481. case '进行中':
  482. return 'status-active';
  483. case '已完成':
  484. return 'status-completed';
  485. case '已延期':
  486. case '已暂停':
  487. return 'status-warning';
  488. case '已解决':
  489. return 'status-success';
  490. case '待处理':
  491. case '处理中':
  492. return 'status-info';
  493. default:
  494. return '';
  495. }
  496. }
  497. }