dashboard.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. import { Component, OnInit, OnDestroy } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { RouterModule } from '@angular/router';
  4. import { ProjectService } from '../../../services/project.service';
  5. import { Task } from '../../../models/project.model';
  6. import { SkillRadarComponent } from './skill-radar/skill-radar.component';
  7. import { PersonalBoard } from '../personal-board/personal-board';
  8. import { FmodeQuery, FmodeObject, FmodeUser } from 'fmode-ng/core';
  9. import { WxworkAuth } from 'fmode-ng/core';
  10. interface ShiftTask {
  11. id: string;
  12. projectId: string;
  13. projectName: string;
  14. taskDescription: string;
  15. priority: '高' | '中' | '低';
  16. shiftDate: string;
  17. status: '待处理' | '处理中' | '已完成';
  18. }
  19. interface ProjectTimelineItem {
  20. id: string;
  21. name: string;
  22. deadline: string;
  23. status: string;
  24. }
  25. @Component({
  26. selector: 'app-dashboard',
  27. standalone: true,
  28. imports: [CommonModule, RouterModule, SkillRadarComponent, PersonalBoard],
  29. templateUrl: './dashboard.html',
  30. styleUrl: './dashboard.scss'
  31. })
  32. export class Dashboard implements OnInit {
  33. // 视图管理
  34. activeDashboard: 'main' | 'skills' | 'personal' = 'main';
  35. // 新增:工作台视图模式(卡片/列表)
  36. viewMode: 'card' | 'list' = 'list';
  37. tasks: Task[] = [];
  38. overdueTasks: Task[] = [];
  39. urgentTasks: Task[] = [];
  40. pendingFeedbacks: {task: Task, feedback: any}[] = [];
  41. reminderMessage: string = '';
  42. feedbackProjectId: string = '';
  43. countdowns: Map<string, string> = new Map();
  44. // 代班信息相关属性
  45. shiftTasks: ShiftTask[] = [];
  46. // 个人项目饱和度相关属性
  47. workloadPercentage: number = 0;
  48. projectTimeline: ProjectTimelineItem[] = [];
  49. private wxAuth: WxworkAuth | null = null;
  50. private currentUser: FmodeUser | null = null;
  51. constructor(private projectService: ProjectService) {
  52. this.initAuth();
  53. }
  54. // 初始化企业微信认证
  55. private initAuth(): void {
  56. try {
  57. this.wxAuth = new WxworkAuth({
  58. cid: 'cDL6R1hgSi' // 公司帐套ID
  59. });
  60. console.log('✅ 设计师仪表板企业微信认证初始化成功');
  61. } catch (error) {
  62. console.error('❌ 设计师仪表板企业微信认证初始化失败:', error);
  63. }
  64. }
  65. async ngOnInit(): Promise<void> {
  66. await this.authenticateAndLoadData();
  67. }
  68. // 认证并加载数据
  69. private async authenticateAndLoadData(): Promise<void> {
  70. try {
  71. // 执行企业微信认证和登录
  72. const { user } = await this.wxAuth!.authenticateAndLogin();
  73. this.currentUser = user;
  74. if (user) {
  75. console.log('✅ 设计师登录成功:', user.get('username'));
  76. await this.loadDashboardData();
  77. } else {
  78. console.error('❌ 设计师登录失败');
  79. }
  80. } catch (error) {
  81. console.error('❌ 设计师认证过程出错:', error);
  82. // 降级到模拟数据
  83. this.loadMockData();
  84. }
  85. }
  86. // 加载仪表板数据
  87. private async loadDashboardData(): Promise<void> {
  88. try {
  89. await Promise.all([
  90. this.loadTasks(),
  91. this.loadShiftTasks(),
  92. this.calculateWorkloadPercentage(),
  93. this.loadProjectTimeline()
  94. ]);
  95. console.log('✅ 设计师仪表板数据加载完成');
  96. } catch (error) {
  97. console.error('❌ 设计师仪表板数据加载失败:', error);
  98. throw error;
  99. }
  100. }
  101. // 降级到模拟数据
  102. private loadMockData(): void {
  103. console.warn('⚠️ 使用模拟数据');
  104. this.loadTasks();
  105. this.loadShiftTasks();
  106. this.calculateWorkloadPercentage();
  107. this.loadProjectTimeline();
  108. }
  109. // 切换视图方法
  110. switchDashboard(view: 'main' | 'skills' | 'personal'): void {
  111. this.activeDashboard = view;
  112. }
  113. // 新增:切换卡片/列表视图
  114. toggleView(): void {
  115. this.viewMode = this.viewMode === 'card' ? 'list' : 'card';
  116. }
  117. // 获取前N个任务的方法
  118. getTopTasks(count: number): Task[] {
  119. // 过滤掉紧急任务和超期任务
  120. const regularTasks = this.tasks.filter(task =>
  121. !this.urgentTasks.some(urgent => urgent.id === task.id) &&
  122. !this.overdueTasks.some(overdue => overdue.id === task.id)
  123. );
  124. // 返回指定数量的任务
  125. return regularTasks.slice(0, count);
  126. }
  127. // 获取工作量颜色的方法
  128. getWorkloadColor(): string {
  129. if (this.workloadPercentage >= 80) {
  130. return '#ff4560'; // 红色
  131. } else if (this.workloadPercentage >= 50) {
  132. return '#ffa726'; // 橙色
  133. } else {
  134. return '#66bb6a'; // 绿色
  135. }
  136. }
  137. // 获取工作量状态的方法
  138. getWorkloadStatus(): string {
  139. if (this.workloadPercentage >= 80) {
  140. return '工作量饱和';
  141. } else if (this.workloadPercentage >= 50) {
  142. return '工作量适中';
  143. } else if (this.workloadPercentage > 0) {
  144. return '工作量轻松';
  145. } else {
  146. return '暂无工作任务';
  147. }
  148. }
  149. loadTasks(): void {
  150. this.projectService.getTasks().subscribe(tasks => {
  151. // 按阶段优先级排序:建模 > 渲染 > 对图 > 反馈处理 > 后期 > 其他
  152. this.tasks = tasks.sort((a, b) => {
  153. const stagePriority: Record<string, number> = {
  154. '建模': 5,
  155. '渲染': 4,
  156. '对图': 3,
  157. '反馈处理': 2,
  158. '后期': 1,
  159. '投诉处理': 0
  160. };
  161. const priorityA = stagePriority[a.stage] || 0;
  162. const priorityB = stagePriority[b.stage] || 0;
  163. if (priorityA !== priorityB) {
  164. return priorityB - priorityA;
  165. }
  166. // 优先级相同时,按截止日期排序
  167. return a.deadline.getTime() - b.deadline.getTime();
  168. });
  169. // 筛选超期任务
  170. this.overdueTasks = this.tasks.filter(task => task.isOverdue);
  171. // 筛选紧急任务(渲染超时预警,交付前3小时/1小时)
  172. this.urgentTasks = this.tasks.filter(task => {
  173. const now = new Date();
  174. const diffHours = (task.deadline.getTime() - now.getTime()) / (1000 * 60 * 60);
  175. return diffHours <= 3 && diffHours > 0 && task.stage === '渲染';
  176. });
  177. // 设置反馈项目ID
  178. if (this.overdueTasks.length > 0) {
  179. this.feedbackProjectId = this.overdueTasks[0].projectId;
  180. }
  181. // 加载待处理反馈
  182. this.loadPendingFeedbacks();
  183. // 启动倒计时
  184. this.startCountdowns();
  185. });
  186. }
  187. loadPendingFeedbacks(): void {
  188. this.pendingFeedbacks = [];
  189. // 模拟加载待处理反馈数据
  190. this.tasks.forEach(task => {
  191. // 使用模拟数据代替API调用
  192. const mockFeedbacks = [
  193. {
  194. id: 'fb-' + task.id,
  195. projectId: task.projectId,
  196. content: '客户对色彩不满意,需要调整',
  197. isSatisfied: false,
  198. problemLocation: '色彩',
  199. expectedEffect: '更明亮的色调',
  200. referenceCase: '无',
  201. status: '待处理' as const,
  202. createdAt: new Date(Date.now() - 30 * 60 * 1000) // 30分钟前
  203. },
  204. {
  205. id: 'fb-' + task.id + '-2',
  206. projectId: task.projectId,
  207. content: '家具款式需要调整',
  208. isSatisfied: false,
  209. problemLocation: '家具',
  210. expectedEffect: '更现代的款式',
  211. referenceCase: '无',
  212. status: '待处理' as const,
  213. createdAt: new Date(Date.now() - 45 * 60 * 1000) // 45分钟前
  214. }
  215. ];
  216. const pending = mockFeedbacks.filter(feedback =>
  217. feedback.status === '待处理' &&
  218. !feedback.isSatisfied
  219. );
  220. if (pending.length > 0) {
  221. this.pendingFeedbacks.push({task, feedback: pending[0]});
  222. }
  223. });
  224. }
  225. startCountdowns(): void {
  226. // 清除之前的定时器
  227. this.countdowns.clear();
  228. // 为所有任务启动倒计时,确保列表视图也有剩余时间显示
  229. this.tasks.forEach(task => {
  230. this.updateCountdown(task.id, task.deadline);
  231. });
  232. // 定期更新倒计时
  233. setInterval(() => {
  234. this.tasks.forEach(task => {
  235. this.updateCountdown(task.id, task.deadline);
  236. });
  237. }, 60000); // 每分钟更新一次
  238. }
  239. updateCountdown(taskId: string, deadline: Date): void {
  240. const now = new Date();
  241. const diffMs = deadline.getTime() - now.getTime();
  242. if (diffMs <= 0) {
  243. this.countdowns.set(taskId, '已超期');
  244. return;
  245. }
  246. const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
  247. const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
  248. if (diffHours > 0) {
  249. this.countdowns.set(taskId, `${diffHours}小时${diffMinutes}分钟`);
  250. } else {
  251. this.countdowns.set(taskId, `${diffMinutes}分钟`);
  252. }
  253. }
  254. getTaskCountdown(taskId: string): string {
  255. return this.countdowns.get(taskId) || '';
  256. }
  257. // 新增:列表视图专用剩余时间格式化(若未在countdowns中,直接计算)
  258. getListTimeLeft(task: Task): string {
  259. const cached = this.getTaskCountdown(task.id);
  260. if (cached) return cached;
  261. const now = new Date();
  262. const diffMs = task.deadline.getTime() - now.getTime();
  263. if (diffMs <= 0) return '已超期';
  264. const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
  265. const diffHours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
  266. if (diffDays > 0) return `${diffDays}天${diffHours}小时`;
  267. const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
  268. if (diffHours > 0) return `${diffHours}小时${diffMinutes}分钟`;
  269. return `${diffMinutes}分钟`;
  270. }
  271. // 新增:按紧急度排序
  272. getTasksSortedByUrgency(): Task[] {
  273. return this.tasks
  274. .filter(t => !t.isCompleted)
  275. .slice()
  276. .sort((a, b) => this.getUrgencyScore(b) - this.getUrgencyScore(a));
  277. }
  278. // 新增:紧急度评分,数值越大越紧急
  279. private getUrgencyScore(task: Task): number {
  280. if (task.isOverdue) return 10000;
  281. const now = new Date().getTime();
  282. const hoursLeft = (task.deadline.getTime() - now) / (1000 * 60 * 60);
  283. let base = 0;
  284. if (hoursLeft <= 3) base = 9000;
  285. else if (hoursLeft <= 24) base = 7000;
  286. else if (hoursLeft <= 72) base = 4000;
  287. else base = 1000;
  288. // 渲染阶段适度提高权重
  289. const stageBoost = task.stage === '渲染' ? 300 : 0;
  290. return base + stageBoost;
  291. }
  292. // 新增:紧急度标签
  293. getUrgencyLevel(task: Task): '超期' | '高' | '中' | '低' {
  294. if (task.isOverdue) return '超期';
  295. const now = new Date().getTime();
  296. const hoursLeft = (task.deadline.getTime() - now) / (1000 * 60 * 60);
  297. if (hoursLeft <= 24) return '高';
  298. if (hoursLeft <= 72) return '中';
  299. return '低';
  300. }
  301. // 新增:紧急度样式类
  302. getUrgencyClass(task: Task): string {
  303. const level = this.getUrgencyLevel(task);
  304. switch (level) {
  305. case '超期':
  306. return 'urgency-overdue';
  307. case '高':
  308. return 'urgency-high';
  309. case '中':
  310. return 'urgency-medium';
  311. default:
  312. return 'urgency-low';
  313. }
  314. }
  315. getTaskStageProgress(taskId: string): number {
  316. const task = this.tasks.find(t => t.id === taskId);
  317. if (!task) return 0;
  318. // 为不同阶段设置固定的模拟进度值
  319. const stageProgressMap: Record<string, number> = {
  320. '建模': 22,
  321. '渲染': 23,
  322. '对图': 50,
  323. '反馈处理': 80,
  324. '后期': 75,
  325. '投诉处理': 100
  326. };
  327. // 对于渲染任务,如果有实际的渲染进度数据,使用它
  328. if (task.stage === '渲染') {
  329. // 在实际应用中,这里会从服务中获取真实的进度
  330. // this.projectService.getRenderProgress(task.projectId).subscribe(progress => {
  331. // if (progress) {
  332. // return progress.completionRate;
  333. // }
  334. // });
  335. }
  336. return stageProgressMap[task.stage] || 0;
  337. }
  338. markTaskAsCompleted(taskId: string): void {
  339. this.projectService.markTaskAsCompleted(taskId).subscribe(() => {
  340. this.loadTasks(); // 重新加载任务列表
  341. });
  342. }
  343. handleFeedback(taskId: string): void {
  344. const task = this.tasks.find(t => t.id === taskId);
  345. if (task) {
  346. // 跳转到项目详情的反馈处理页面
  347. window.location.href = `/designer/project-detail/${task.projectId}#feedback`;
  348. }
  349. }
  350. generateReminderMessage(): void {
  351. this.projectService.generateReminderMessage('overdue').subscribe(message => {
  352. this.reminderMessage = message;
  353. });
  354. }
  355. clearReminder(): void {
  356. this.reminderMessage = '';
  357. }
  358. // 代班任务相关方法
  359. loadShiftTasks(): void {
  360. // 在实际应用中,这里应该从服务中获取代班任务
  361. // 这里使用模拟数据
  362. this.shiftTasks = [
  363. {
  364. id: 'shift1',
  365. projectId: 'project1',
  366. projectName: '现代风格客厅设计',
  367. taskDescription: '小图修改反馈和渲染进度跟踪',
  368. priority: '高',
  369. shiftDate: '2025-09-15',
  370. status: '待处理'
  371. },
  372. {
  373. id: 'shift2',
  374. projectId: 'project2',
  375. projectName: '北欧风卧室装修',
  376. taskDescription: '查看客户反馈并提供初步修改建议',
  377. priority: '中',
  378. shiftDate: '2025-09-16',
  379. status: '待处理'
  380. },
  381. {
  382. id: 'shift3',
  383. projectId: 'project3',
  384. projectName: '新中式书房改造',
  385. taskDescription: '完成剩余渲染任务',
  386. priority: '低',
  387. shiftDate: '2025-09-17',
  388. status: '处理中'
  389. }
  390. ];
  391. }
  392. // 打开添加代班任务的模态框
  393. openShiftModal(): void {
  394. // 在实际应用中,这里应该打开一个模态框让用户添加代班任务
  395. // 这里使用alert模拟
  396. window.fmode?.alert?.({
  397. header: '添加代班任务',
  398. message: '将打开添加代班任务的表单',
  399. buttons: [{ text: '确定', role: 'confirm' }]
  400. });
  401. // 实际实现可能是:this.modalService.openShiftModal();
  402. }
  403. // 查看代班任务详情
  404. viewShiftDetail(shiftId: string): void {
  405. const shift = this.shiftTasks.find(s => s.id === shiftId);
  406. if (shift) {
  407. // 实际应用中,这里应该打开详情页面或模态框
  408. console.log('查看代班任务详情:', shift);
  409. window?.fmode?.alert(`代班任务详情:\n项目:${shift.projectName}\n任务:${shift.taskDescription}\n优先级:${shift.priority}\n代班日期:${shift.shiftDate}`);
  410. }
  411. }
  412. // 标记代班任务完成
  413. markShiftComplete(shiftId: string): void {
  414. const shiftIndex = this.shiftTasks.findIndex(s => s.id === shiftId);
  415. if (shiftIndex !== -1) {
  416. // 在实际应用中,这里应该调用API更新状态
  417. this.shiftTasks[shiftIndex].status = '已完成';
  418. window?.fmode?.alert('代班任务已标记为完成');
  419. }
  420. }
  421. // 计算项目饱和度
  422. calculateWorkloadPercentage(): void {
  423. // 在实际应用中,这里应该从服务中获取真实的项目饱和度数据
  424. // 这里使用模拟数据,根据当前任务数量计算饱和度
  425. const totalCapacity = 5; // 假设设计师最大同时处理5个项目
  426. const currentProjects = this.tasks.length;
  427. // 计算饱和度百分比
  428. this.workloadPercentage = Math.round((currentProjects / totalCapacity) * 100);
  429. // 确保百分比在0-100之间
  430. this.workloadPercentage = Math.min(Math.max(this.workloadPercentage, 0), 100);
  431. }
  432. // 加载项目排期表
  433. loadProjectTimeline(): void {
  434. // 在实际应用中,这里应该从服务中获取项目排期数据
  435. // 这里使用模拟数据
  436. this.projectTimeline = [
  437. {
  438. id: 'timeline1',
  439. name: '现代风格客厅设计',
  440. deadline: '2025-09-20',
  441. status: '进行中'
  442. },
  443. {
  444. id: 'timeline2',
  445. name: '北欧风卧室装修',
  446. deadline: '2025-09-25',
  447. status: '进行中'
  448. },
  449. {
  450. id: 'timeline3',
  451. name: '新中式书房改造',
  452. deadline: '2025-09-30',
  453. status: '进行中'
  454. }
  455. ].sort((a, b) => new Date(a.deadline).getTime() - new Date(b.deadline).getTime());
  456. }
  457. }