designer-calendar.component.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590
  1. import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { FormsModule } from '@angular/forms';
  4. export interface Designer {
  5. id: string;
  6. name: string;
  7. avatar?: string;
  8. groupId: string;
  9. groupName: string;
  10. isLeader: boolean;
  11. status: 'available' | 'busy' | 'stagnant' | 'overloaded';
  12. currentProjects: number;
  13. lastOrderDate?: string;
  14. idleDays?: number;
  15. completedThisMonth?: number;
  16. averageCycle?: number;
  17. upcomingEvents?: DesignerEvent[];
  18. workload?: number; // 0-100
  19. nextAvailableDate?: Date;
  20. }
  21. export interface DesignerEvent {
  22. id: string;
  23. date: Date;
  24. title: string;
  25. type: 'project' | 'review' | 'vacation';
  26. projectId?: string;
  27. duration: number; // 小时
  28. }
  29. export interface ProjectGroup {
  30. id: string;
  31. name: string;
  32. leaderId: string;
  33. memberIds: string[];
  34. }
  35. @Component({
  36. selector: 'app-designer-calendar',
  37. standalone: true,
  38. imports: [CommonModule, FormsModule],
  39. templateUrl: './designer-calendar.component.html',
  40. styleUrls: ['./designer-calendar.component.scss']
  41. })
  42. export class DesignerCalendarComponent implements OnInit {
  43. @Input() designers: Designer[] = [];
  44. @Input() selectedDate: Date = new Date();
  45. @Input() projectGroups: ProjectGroup[] = [];
  46. @Input() showSingleDesigner: boolean = false; // 是否显示单个设计师模式
  47. @Input() timeRange: 'week' | 'month' | 'quarter' = 'month'; // 时间范围视图,支持外部传入
  48. @Output() designerSelected = new EventEmitter<Designer>();
  49. @Output() assignmentRequested = new EventEmitter<Designer>();
  50. // 视图模式
  51. viewMode: 'calendar' | 'list' = 'calendar';
  52. // 筛选条件
  53. selectedGroup: string = '';
  54. selectedStatus: string = '';
  55. hideStagnantProjects: boolean = false;
  56. // 新增:设计师名称搜索
  57. designerSearch: string = '';
  58. // 头像默认回退路径
  59. defaultAvatarPath: string = document.baseURI+'/assets/images/default-avatar.svg';
  60. // 日历数据
  61. currentDate: Date = new Date();
  62. calendarDates: Date[] = [];
  63. filteredDesigners: Designer[] = [];
  64. ngOnInit() {
  65. this.initializeMockData();
  66. this.generateCalendarDates();
  67. this.applyFilters();
  68. }
  69. private initializeMockData() {
  70. if (this.designers.length === 0) {
  71. this.designers = [
  72. {
  73. id: '1',
  74. name: '张设计师',
  75. avatar: document.baseURI+'/assets/avatars/designer1.jpg',
  76. groupId: '1',
  77. groupName: '家装组',
  78. isLeader: true,
  79. status: 'available',
  80. currentProjects: 2,
  81. completedThisMonth: 5,
  82. averageCycle: 15,
  83. idleDays: 3,
  84. workload: 60,
  85. nextAvailableDate: new Date(),
  86. upcomingEvents: [
  87. {
  88. id: '1',
  89. date: new Date(Date.now() + 86400000),
  90. title: '客厅设计方案',
  91. type: 'project',
  92. projectId: 'p1',
  93. duration: 4
  94. }
  95. ]
  96. },
  97. {
  98. id: '2',
  99. name: '李设计师',
  100. avatar: document.baseURI+'/assets/avatars/designer2.jpg',
  101. groupId: '2',
  102. groupName: '工装组',
  103. isLeader: false,
  104. status: 'busy',
  105. currentProjects: 4,
  106. completedThisMonth: 3,
  107. averageCycle: 18,
  108. idleDays: 0,
  109. workload: 85,
  110. nextAvailableDate: new Date(Date.now() + 5 * 86400000),
  111. upcomingEvents: [
  112. {
  113. id: '2',
  114. date: new Date(),
  115. title: '办公室设计对图',
  116. type: 'review',
  117. projectId: 'p2',
  118. duration: 2
  119. }
  120. ]
  121. },
  122. {
  123. id: '3',
  124. name: '王设计师',
  125. avatar: document.baseURI+'/assets/avatars/designer3.jpg',
  126. groupId: '1',
  127. groupName: '家装组',
  128. isLeader: false,
  129. status: 'stagnant',
  130. currentProjects: 1,
  131. completedThisMonth: 1,
  132. averageCycle: 25,
  133. idleDays: 15,
  134. workload: 20,
  135. nextAvailableDate: new Date(),
  136. upcomingEvents: []
  137. }
  138. ];
  139. }
  140. if (this.projectGroups.length === 0) {
  141. this.projectGroups = [
  142. { id: '1', name: '家装组', leaderId: '1', memberIds: ['1', '3'] },
  143. { id: '2', name: '工装组', leaderId: '2', memberIds: ['2'] }
  144. ];
  145. }
  146. }
  147. private generateCalendarDates() {
  148. this.calendarDates = [];
  149. const startDate = new Date(this.currentDate);
  150. if (this.timeRange === 'week') {
  151. // 获取本周的日期
  152. const dayOfWeek = startDate.getDay();
  153. startDate.setDate(startDate.getDate() - dayOfWeek);
  154. for (let i = 0; i < 7; i++) {
  155. const date = new Date(startDate);
  156. date.setDate(startDate.getDate() + i);
  157. this.calendarDates.push(date);
  158. }
  159. } else if (this.timeRange === 'month') {
  160. // 获取本月的完整周视图(包括上个月和下个月的部分日期)
  161. startDate.setDate(1);
  162. // 获取本月第一天是星期几(0=周日,1=周一,...,6=周六)
  163. const firstDayOfMonth = startDate.getDay();
  164. // 获取上个月的最后几天
  165. const daysFromPrevMonth = firstDayOfMonth === 0 ? 6 : firstDayOfMonth - 1;
  166. const prevMonth = new Date(startDate);
  167. prevMonth.setMonth(prevMonth.getMonth() - 1);
  168. const daysInPrevMonth = new Date(prevMonth.getFullYear(), prevMonth.getMonth() + 1, 0).getDate();
  169. // 添加上个月的最后几天
  170. for (let i = daysFromPrevMonth; i > 0; i--) {
  171. const date = new Date(startDate);
  172. date.setDate(1 - i);
  173. this.calendarDates.push(date);
  174. }
  175. // 添加本月的所有日期
  176. const daysInMonth = new Date(startDate.getFullYear(), startDate.getMonth() + 1, 0).getDate();
  177. for (let i = 0; i < daysInMonth; i++) {
  178. const date = new Date(startDate);
  179. date.setDate(i + 1);
  180. this.calendarDates.push(date);
  181. }
  182. // 添加下个月的前几天以完成最后一周
  183. const lastDate = new Date(startDate);
  184. lastDate.setDate(daysInMonth);
  185. const lastDayOfMonth = lastDate.getDay();
  186. const daysFromNextMonth = lastDayOfMonth === 0 ? 0 : 7 - lastDayOfMonth;
  187. for (let i = 1; i <= daysFromNextMonth; i++) {
  188. const date = new Date(startDate);
  189. date.setMonth(date.getMonth() + 1);
  190. date.setDate(i);
  191. this.calendarDates.push(date);
  192. }
  193. } else {
  194. // 获取本季度的周数
  195. const quarterStart = new Date(startDate.getFullYear(), Math.floor(startDate.getMonth() / 3) * 3, 1);
  196. const quarterEnd = new Date(startDate.getFullYear(), Math.floor(startDate.getMonth() / 3) * 3 + 3, 0);
  197. const current = new Date(quarterStart);
  198. while (current <= quarterEnd) {
  199. this.calendarDates.push(new Date(current));
  200. current.setDate(current.getDate() + 7); // 按周显示
  201. }
  202. }
  203. }
  204. private applyFilters() {
  205. this.filteredDesigners = this.designers.filter(designer => {
  206. // 项目组筛选
  207. if (this.selectedGroup && designer.groupId !== this.selectedGroup) {
  208. return false;
  209. }
  210. // 状态筛选
  211. if (this.selectedStatus && designer.status !== this.selectedStatus) {
  212. return false;
  213. }
  214. // 停滞期项目筛选
  215. if (this.hideStagnantProjects && designer.status === 'stagnant') {
  216. return false;
  217. }
  218. // 名称搜索筛选
  219. if (this.designerSearch && !designer.name.toLowerCase().includes(this.designerSearch.toLowerCase())) {
  220. return false;
  221. }
  222. return true;
  223. });
  224. }
  225. private getGroupName(groupId: string): string {
  226. const group = this.projectGroups.find(g => g.id === groupId);
  227. return group ? group.name : '';
  228. }
  229. // 视图切换
  230. toggleView() {
  231. this.viewMode = this.viewMode === 'calendar' ? 'list' : 'calendar';
  232. }
  233. // 筛选器事件
  234. onGroupChange() {
  235. this.applyFilters();
  236. }
  237. onStatusChange() {
  238. this.applyFilters();
  239. }
  240. onTimeRangeChange() {
  241. this.generateCalendarDates();
  242. this.applyFilters();
  243. }
  244. onFilterChange() {
  245. this.applyFilters();
  246. }
  247. // 新增:设计师搜索变化
  248. onDesignerSearchChange() {
  249. this.applyFilters();
  250. }
  251. // 日历导航
  252. previousPeriod() {
  253. if (this.timeRange === 'week') {
  254. this.currentDate.setDate(this.currentDate.getDate() - 7);
  255. } else if (this.timeRange === 'month') {
  256. this.currentDate.setMonth(this.currentDate.getMonth() - 1);
  257. } else {
  258. this.currentDate.setMonth(this.currentDate.getMonth() - 3);
  259. }
  260. this.generateCalendarDates();
  261. }
  262. nextPeriod() {
  263. if (this.timeRange === 'week') {
  264. this.currentDate.setDate(this.currentDate.getDate() + 7);
  265. } else if (this.timeRange === 'month') {
  266. this.currentDate.setMonth(this.currentDate.getMonth() + 1);
  267. } else {
  268. this.currentDate.setMonth(this.currentDate.getMonth() + 3);
  269. }
  270. this.generateCalendarDates();
  271. }
  272. getCurrentPeriodLabel(): string {
  273. const year = this.currentDate.getFullYear();
  274. const month = this.currentDate.getMonth() + 1;
  275. if (this.timeRange === 'week') {
  276. const weekStart = new Date(this.currentDate);
  277. weekStart.setDate(this.currentDate.getDate() - this.currentDate.getDay());
  278. const weekEnd = new Date(weekStart);
  279. weekEnd.setDate(weekStart.getDate() + 6);
  280. return `${weekStart.getMonth() + 1}/${weekStart.getDate()} - ${weekEnd.getMonth() + 1}/${weekEnd.getDate()}`;
  281. } else if (this.timeRange === 'month') {
  282. return `${year}年${month}月`;
  283. } else {
  284. const quarter = Math.floor(month / 3) + 1;
  285. return `${year}年第${quarter}季度`;
  286. }
  287. }
  288. // 日期格式化
  289. formatDate(date: Date, format: string): string {
  290. const options: Intl.DateTimeFormatOptions = {};
  291. if (format === 'MM/dd') {
  292. return `${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getDate().toString().padStart(2, '0')}`;
  293. } else if (format === 'EEE') {
  294. const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
  295. return weekdays[date.getDay()];
  296. }
  297. return date.toLocaleDateString();
  298. }
  299. // 获取日期事件
  300. getDateEvents(designer: Designer, date: Date): DesignerEvent[] {
  301. if (!designer.upcomingEvents) return [];
  302. return designer.upcomingEvents.filter(event => {
  303. const eventDate = new Date(event.date);
  304. return eventDate.toDateString() === date.toDateString();
  305. });
  306. }
  307. // 计算当天可用性等级(free/partial/booked)
  308. getAvailabilityLevel(designer: Designer, date: Date): 'free' | 'partial' | 'booked' {
  309. const events = this.getDateEvents(designer, date);
  310. const totalHours = events.reduce((sum, e) => sum + (e.duration || 0), 0);
  311. if (totalHours === 0) return 'free';
  312. if (totalHours < 6) return 'partial';
  313. return 'booked';
  314. }
  315. // 获取日期单元格样式
  316. getDateCellClass(designer: Designer, date: Date): string {
  317. const events = this.getDateEvents(designer, date);
  318. const classes: string[] = [];
  319. // 新增:按日可用性颜色编码
  320. const level = this.getAvailabilityLevel(designer, date);
  321. classes.push(`availability-${level}`);
  322. if (events.length > 0) {
  323. classes.push('has-events');
  324. if (events.some(e => e.type === 'review')) {
  325. classes.push('review-day');
  326. }
  327. if (events.some(e => e.type === 'project')) {
  328. classes.push('project-day');
  329. }
  330. if (events.some(e => e.type === 'vacation')) {
  331. classes.push('vacation-day');
  332. }
  333. } else {
  334. classes.push('available-day');
  335. }
  336. // 今天
  337. if (date.toDateString() === new Date().toDateString()) {
  338. classes.push('today');
  339. }
  340. return classes.join(' ');
  341. }
  342. // 获取事件样式
  343. getEventClass(event: DesignerEvent): string {
  344. return `event-${event.type}`;
  345. }
  346. // 获取状态文本
  347. getStatusText(status: string): string {
  348. const statusMap = {
  349. 'available': '空闲',
  350. 'busy': '忙碌',
  351. 'overloaded': '满载',
  352. 'stagnant': '停滞期'
  353. };
  354. return statusMap[status as keyof typeof statusMap] || status;
  355. }
  356. // 获取事件类型文本
  357. getEventTypeText(type: string): string {
  358. const typeMap = {
  359. 'project': '项目工作',
  360. 'review': '对图时间',
  361. 'vacation': '休假'
  362. };
  363. return typeMap[type as keyof typeof typeMap] || type;
  364. }
  365. // 获取空闲天数样式
  366. getIdleDaysClass(days: number): string {
  367. if (days >= 10) return 'critical';
  368. if (days >= 5) return 'warning';
  369. return 'normal';
  370. }
  371. // 获取下次可接单日期
  372. getNextAvailableDate(designer: Designer): string {
  373. if (!designer.nextAvailableDate) return '立即可用';
  374. const nextDate = new Date(designer.nextAvailableDate);
  375. const today = new Date();
  376. const diffTime = nextDate.getTime() - today.getTime();
  377. const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
  378. if (diffDays <= 0) return '立即可用';
  379. if (diffDays === 1) return '明天';
  380. return `${diffDays}天后`;
  381. }
  382. // 获取推荐分配等级
  383. getRecommendationClass(designer: Designer): string {
  384. if (designer.status === 'available' && (designer.idleDays ?? 0) >= 5) return 'high-recommend';
  385. if (designer.status === 'available') return 'recommend';
  386. if (designer.status === 'busy') return 'caution';
  387. return 'not-recommend';
  388. }
  389. // 获取推荐分配文本
  390. getRecommendationText(designer: Designer): string {
  391. if (designer.status === 'available' && (designer.idleDays ?? 0) >= 5) return '强烈推荐';
  392. if (designer.status === 'available') return '推荐';
  393. if (designer.status === 'busy') return '谨慎分配';
  394. return '不推荐';
  395. }
  396. // 统计方法
  397. getDesignerCountByStatus(status: string): number {
  398. return this.filteredDesigners.filter(d => d.status === status).length;
  399. }
  400. getStagnantProjectsCount(): number {
  401. return this.filteredDesigners
  402. .filter(d => d.status === 'stagnant')
  403. .reduce((sum, d) => sum + d.currentProjects, 0);
  404. }
  405. // 新增:统计今日的按日可用性数量
  406. getAvailabilityCount(level: 'free' | 'partial' | 'booked'): number {
  407. return this.filteredDesigners.filter(d => this.getAvailabilityLevel(d, this.currentDate) === level).length;
  408. }
  409. // 操作方法
  410. refreshData() {
  411. // 刷新数据逻辑
  412. this.applyFilters();
  413. }
  414. viewDesignerDetail(designer: Designer) {
  415. this.designerSelected.emit(designer);
  416. }
  417. assignToDesigner(designer: Designer) {
  418. this.assignmentRequested.emit(designer);
  419. }
  420. // 头像加载失败回退
  421. onAvatarError(event: Event) {
  422. const img = event.target as HTMLImageElement;
  423. img.src = this.defaultAvatarPath;
  424. }
  425. // ========== 月日历视图相关方法 ==========
  426. // 判断是否是今天
  427. isToday(date: Date): boolean {
  428. const today = new Date();
  429. return date.getDate() === today.getDate() &&
  430. date.getMonth() === today.getMonth() &&
  431. date.getFullYear() === today.getFullYear();
  432. }
  433. // 判断是否是当前月份
  434. isCurrentMonth(date: Date): boolean {
  435. return date.getMonth() === this.currentDate.getMonth() &&
  436. date.getFullYear() === this.currentDate.getFullYear();
  437. }
  438. // 判断是否是周末
  439. isWeekend(date: Date): boolean {
  440. const day = date.getDay();
  441. return day === 0 || day === 6;
  442. }
  443. // 获取设计师在指定日期的状态CSS类
  444. getDesignerDayStatusClass(designer: Designer, date: Date): string {
  445. const classes: string[] = [];
  446. if (this.isDateAvailable(designer, date)) {
  447. classes.push('available');
  448. }
  449. if (this.isDateReview(designer, date)) {
  450. classes.push('review');
  451. }
  452. if (this.isDateBusy(designer, date)) {
  453. classes.push('busy');
  454. }
  455. return classes.join(' ');
  456. }
  457. // 获取设计师在指定日期的状态标题
  458. getDesignerDayStatusTitle(designer: Designer, date: Date): string {
  459. const statuses: string[] = [];
  460. statuses.push(`${designer.name}`);
  461. if (this.isDateAvailable(designer, date)) {
  462. statuses.push('空闲可接单');
  463. }
  464. if (this.isDateReview(designer, date)) {
  465. statuses.push('对图日');
  466. }
  467. if (this.isDateBusy(designer, date)) {
  468. statuses.push('忙碌中');
  469. }
  470. const events = this.getDateEvents(designer, date);
  471. if (events.length > 0) {
  472. statuses.push(`${events.length}个事件`);
  473. }
  474. return statuses.join(' · ');
  475. }
  476. // 判断设计师在指定日期是否空闲
  477. isDateAvailable(designer: Designer, date: Date): boolean {
  478. // 如果设计师状态是available且该日期没有事件,则认为空闲
  479. if (designer.status === 'available') {
  480. const events = this.getDateEvents(designer, date);
  481. return events.length === 0;
  482. }
  483. return false;
  484. }
  485. // 判断设计师在指定日期是否对图
  486. isDateReview(designer: Designer, date: Date): boolean {
  487. const dateStr = this.formatDateString(date);
  488. // 从upcomingEvents中查找对图事件
  489. const events = designer.upcomingEvents || [];
  490. return events.some(e => e.type === 'review' && this.formatDateString(e.date) === dateStr);
  491. }
  492. // 判断设计师在指定日期是否忙碌
  493. isDateBusy(designer: Designer, date: Date): boolean {
  494. const events = this.getDateEvents(designer, date);
  495. return events.some(e => e.type === 'project');
  496. }
  497. // 格式化日期为字符串(用于比较)
  498. formatDateString(date: Date): string {
  499. const year = date.getFullYear();
  500. const month = (date.getMonth() + 1).toString().padStart(2, '0');
  501. const day = date.getDate().toString().padStart(2, '0');
  502. return `${year}-${month}-${day}`;
  503. }
  504. // 获取工作量CSS类
  505. getWorkloadClass(workload: number): string {
  506. if (workload >= 80) return 'high';
  507. if (workload >= 50) return 'medium';
  508. return 'low';
  509. }
  510. }