| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590 |
- import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
- import { CommonModule } from '@angular/common';
- import { FormsModule } from '@angular/forms';
- export interface Designer {
- id: string;
- name: string;
- avatar?: string;
- groupId: string;
- groupName: string;
- isLeader: boolean;
- status: 'available' | 'busy' | 'stagnant' | 'overloaded';
- currentProjects: number;
- lastOrderDate?: string;
- idleDays?: number;
- completedThisMonth?: number;
- averageCycle?: number;
- upcomingEvents?: DesignerEvent[];
- workload?: number; // 0-100
- nextAvailableDate?: Date;
- }
- export interface DesignerEvent {
- id: string;
- date: Date;
- title: string;
- type: 'project' | 'review' | 'vacation';
- projectId?: string;
- duration: number; // 小时
- }
- export interface ProjectGroup {
- id: string;
- name: string;
- leaderId: string;
- memberIds: string[];
- }
- @Component({
- selector: 'app-designer-calendar',
- standalone: true,
- imports: [CommonModule, FormsModule],
- templateUrl: './designer-calendar.component.html',
- styleUrls: ['./designer-calendar.component.scss']
- })
- export class DesignerCalendarComponent implements OnInit {
- @Input() designers: Designer[] = [];
- @Input() selectedDate: Date = new Date();
- @Input() projectGroups: ProjectGroup[] = [];
- @Input() showSingleDesigner: boolean = false; // 是否显示单个设计师模式
- @Input() timeRange: 'week' | 'month' | 'quarter' = 'month'; // 时间范围视图,支持外部传入
- @Output() designerSelected = new EventEmitter<Designer>();
- @Output() assignmentRequested = new EventEmitter<Designer>();
- // 视图模式
- viewMode: 'calendar' | 'list' = 'calendar';
-
- // 筛选条件
- selectedGroup: string = '';
- selectedStatus: string = '';
- hideStagnantProjects: boolean = false;
- // 新增:设计师名称搜索
- designerSearch: string = '';
- // 头像默认回退路径
- defaultAvatarPath: string = document.baseURI+'/assets/images/default-avatar.svg';
-
- // 日历数据
- currentDate: Date = new Date();
- calendarDates: Date[] = [];
- filteredDesigners: Designer[] = [];
- ngOnInit() {
- this.initializeMockData();
- this.generateCalendarDates();
- this.applyFilters();
- }
- private initializeMockData() {
- if (this.designers.length === 0) {
- this.designers = [
- {
- id: '1',
- name: '张设计师',
- avatar: document.baseURI+'/assets/avatars/designer1.jpg',
- groupId: '1',
- groupName: '家装组',
- isLeader: true,
- status: 'available',
- currentProjects: 2,
- completedThisMonth: 5,
- averageCycle: 15,
- idleDays: 3,
- workload: 60,
- nextAvailableDate: new Date(),
- upcomingEvents: [
- {
- id: '1',
- date: new Date(Date.now() + 86400000),
- title: '客厅设计方案',
- type: 'project',
- projectId: 'p1',
- duration: 4
- }
- ]
- },
- {
- id: '2',
- name: '李设计师',
- avatar: document.baseURI+'/assets/avatars/designer2.jpg',
- groupId: '2',
- groupName: '工装组',
- isLeader: false,
- status: 'busy',
- currentProjects: 4,
- completedThisMonth: 3,
- averageCycle: 18,
- idleDays: 0,
- workload: 85,
- nextAvailableDate: new Date(Date.now() + 5 * 86400000),
- upcomingEvents: [
- {
- id: '2',
- date: new Date(),
- title: '办公室设计对图',
- type: 'review',
- projectId: 'p2',
- duration: 2
- }
- ]
- },
- {
- id: '3',
- name: '王设计师',
- avatar: document.baseURI+'/assets/avatars/designer3.jpg',
- groupId: '1',
- groupName: '家装组',
- isLeader: false,
- status: 'stagnant',
- currentProjects: 1,
- completedThisMonth: 1,
- averageCycle: 25,
- idleDays: 15,
- workload: 20,
- nextAvailableDate: new Date(),
- upcomingEvents: []
- }
- ];
- }
- if (this.projectGroups.length === 0) {
- this.projectGroups = [
- { id: '1', name: '家装组', leaderId: '1', memberIds: ['1', '3'] },
- { id: '2', name: '工装组', leaderId: '2', memberIds: ['2'] }
- ];
- }
- }
- private generateCalendarDates() {
- this.calendarDates = [];
- const startDate = new Date(this.currentDate);
-
- if (this.timeRange === 'week') {
- // 获取本周的日期
- const dayOfWeek = startDate.getDay();
- startDate.setDate(startDate.getDate() - dayOfWeek);
-
- for (let i = 0; i < 7; i++) {
- const date = new Date(startDate);
- date.setDate(startDate.getDate() + i);
- this.calendarDates.push(date);
- }
- } else if (this.timeRange === 'month') {
- // 获取本月的完整周视图(包括上个月和下个月的部分日期)
- startDate.setDate(1);
-
- // 获取本月第一天是星期几(0=周日,1=周一,...,6=周六)
- const firstDayOfMonth = startDate.getDay();
-
- // 获取上个月的最后几天
- const daysFromPrevMonth = firstDayOfMonth === 0 ? 6 : firstDayOfMonth - 1;
- const prevMonth = new Date(startDate);
- prevMonth.setMonth(prevMonth.getMonth() - 1);
- const daysInPrevMonth = new Date(prevMonth.getFullYear(), prevMonth.getMonth() + 1, 0).getDate();
-
- // 添加上个月的最后几天
- for (let i = daysFromPrevMonth; i > 0; i--) {
- const date = new Date(startDate);
- date.setDate(1 - i);
- this.calendarDates.push(date);
- }
-
- // 添加本月的所有日期
- const daysInMonth = new Date(startDate.getFullYear(), startDate.getMonth() + 1, 0).getDate();
- for (let i = 0; i < daysInMonth; i++) {
- const date = new Date(startDate);
- date.setDate(i + 1);
- this.calendarDates.push(date);
- }
-
- // 添加下个月的前几天以完成最后一周
- const lastDate = new Date(startDate);
- lastDate.setDate(daysInMonth);
- const lastDayOfMonth = lastDate.getDay();
- const daysFromNextMonth = lastDayOfMonth === 0 ? 0 : 7 - lastDayOfMonth;
-
- for (let i = 1; i <= daysFromNextMonth; i++) {
- const date = new Date(startDate);
- date.setMonth(date.getMonth() + 1);
- date.setDate(i);
- this.calendarDates.push(date);
- }
- } else {
- // 获取本季度的周数
- const quarterStart = new Date(startDate.getFullYear(), Math.floor(startDate.getMonth() / 3) * 3, 1);
- const quarterEnd = new Date(startDate.getFullYear(), Math.floor(startDate.getMonth() / 3) * 3 + 3, 0);
-
- const current = new Date(quarterStart);
- while (current <= quarterEnd) {
- this.calendarDates.push(new Date(current));
- current.setDate(current.getDate() + 7); // 按周显示
- }
- }
- }
- private applyFilters() {
- this.filteredDesigners = this.designers.filter(designer => {
- // 项目组筛选
- if (this.selectedGroup && designer.groupId !== this.selectedGroup) {
- return false;
- }
-
- // 状态筛选
- if (this.selectedStatus && designer.status !== this.selectedStatus) {
- return false;
- }
-
- // 停滞期项目筛选
- if (this.hideStagnantProjects && designer.status === 'stagnant') {
- return false;
- }
- // 名称搜索筛选
- if (this.designerSearch && !designer.name.toLowerCase().includes(this.designerSearch.toLowerCase())) {
- return false;
- }
-
- return true;
- });
- }
- private getGroupName(groupId: string): string {
- const group = this.projectGroups.find(g => g.id === groupId);
- return group ? group.name : '';
- }
- // 视图切换
- toggleView() {
- this.viewMode = this.viewMode === 'calendar' ? 'list' : 'calendar';
- }
- // 筛选器事件
- onGroupChange() {
- this.applyFilters();
- }
- onStatusChange() {
- this.applyFilters();
- }
- onTimeRangeChange() {
- this.generateCalendarDates();
- this.applyFilters();
- }
- onFilterChange() {
- this.applyFilters();
- }
- // 新增:设计师搜索变化
- onDesignerSearchChange() {
- this.applyFilters();
- }
- // 日历导航
- previousPeriod() {
- if (this.timeRange === 'week') {
- this.currentDate.setDate(this.currentDate.getDate() - 7);
- } else if (this.timeRange === 'month') {
- this.currentDate.setMonth(this.currentDate.getMonth() - 1);
- } else {
- this.currentDate.setMonth(this.currentDate.getMonth() - 3);
- }
- this.generateCalendarDates();
- }
- nextPeriod() {
- if (this.timeRange === 'week') {
- this.currentDate.setDate(this.currentDate.getDate() + 7);
- } else if (this.timeRange === 'month') {
- this.currentDate.setMonth(this.currentDate.getMonth() + 1);
- } else {
- this.currentDate.setMonth(this.currentDate.getMonth() + 3);
- }
- this.generateCalendarDates();
- }
- getCurrentPeriodLabel(): string {
- const year = this.currentDate.getFullYear();
- const month = this.currentDate.getMonth() + 1;
-
- if (this.timeRange === 'week') {
- const weekStart = new Date(this.currentDate);
- weekStart.setDate(this.currentDate.getDate() - this.currentDate.getDay());
- const weekEnd = new Date(weekStart);
- weekEnd.setDate(weekStart.getDate() + 6);
-
- return `${weekStart.getMonth() + 1}/${weekStart.getDate()} - ${weekEnd.getMonth() + 1}/${weekEnd.getDate()}`;
- } else if (this.timeRange === 'month') {
- return `${year}年${month}月`;
- } else {
- const quarter = Math.floor(month / 3) + 1;
- return `${year}年第${quarter}季度`;
- }
- }
- // 日期格式化
- formatDate(date: Date, format: string): string {
- const options: Intl.DateTimeFormatOptions = {};
-
- if (format === 'MM/dd') {
- return `${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getDate().toString().padStart(2, '0')}`;
- } else if (format === 'EEE') {
- const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
- return weekdays[date.getDay()];
- }
-
- return date.toLocaleDateString();
- }
- // 获取日期事件
- getDateEvents(designer: Designer, date: Date): DesignerEvent[] {
- if (!designer.upcomingEvents) return [];
- return designer.upcomingEvents.filter(event => {
- const eventDate = new Date(event.date);
- return eventDate.toDateString() === date.toDateString();
- });
- }
- // 计算当天可用性等级(free/partial/booked)
- getAvailabilityLevel(designer: Designer, date: Date): 'free' | 'partial' | 'booked' {
- const events = this.getDateEvents(designer, date);
- const totalHours = events.reduce((sum, e) => sum + (e.duration || 0), 0);
- if (totalHours === 0) return 'free';
- if (totalHours < 6) return 'partial';
- return 'booked';
- }
- // 获取日期单元格样式
- getDateCellClass(designer: Designer, date: Date): string {
- const events = this.getDateEvents(designer, date);
- const classes: string[] = [];
- // 新增:按日可用性颜色编码
- const level = this.getAvailabilityLevel(designer, date);
- classes.push(`availability-${level}`);
-
- if (events.length > 0) {
- classes.push('has-events');
-
- if (events.some(e => e.type === 'review')) {
- classes.push('review-day');
- }
- if (events.some(e => e.type === 'project')) {
- classes.push('project-day');
- }
- if (events.some(e => e.type === 'vacation')) {
- classes.push('vacation-day');
- }
- } else {
- classes.push('available-day');
- }
-
- // 今天
- if (date.toDateString() === new Date().toDateString()) {
- classes.push('today');
- }
-
- return classes.join(' ');
- }
- // 获取事件样式
- getEventClass(event: DesignerEvent): string {
- return `event-${event.type}`;
- }
- // 获取状态文本
- getStatusText(status: string): string {
- const statusMap = {
- 'available': '空闲',
- 'busy': '忙碌',
- 'overloaded': '满载',
- 'stagnant': '停滞期'
- };
- return statusMap[status as keyof typeof statusMap] || status;
- }
- // 获取事件类型文本
- getEventTypeText(type: string): string {
- const typeMap = {
- 'project': '项目工作',
- 'review': '对图时间',
- 'vacation': '休假'
- };
- return typeMap[type as keyof typeof typeMap] || type;
- }
- // 获取空闲天数样式
- getIdleDaysClass(days: number): string {
- if (days >= 10) return 'critical';
- if (days >= 5) return 'warning';
- return 'normal';
- }
- // 获取下次可接单日期
- getNextAvailableDate(designer: Designer): string {
- if (!designer.nextAvailableDate) return '立即可用';
- const nextDate = new Date(designer.nextAvailableDate);
- const today = new Date();
- const diffTime = nextDate.getTime() - today.getTime();
- const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
-
- if (diffDays <= 0) return '立即可用';
- if (diffDays === 1) return '明天';
- return `${diffDays}天后`;
- }
- // 获取推荐分配等级
- getRecommendationClass(designer: Designer): string {
- if (designer.status === 'available' && (designer.idleDays ?? 0) >= 5) return 'high-recommend';
- if (designer.status === 'available') return 'recommend';
- if (designer.status === 'busy') return 'caution';
- return 'not-recommend';
- }
- // 获取推荐分配文本
- getRecommendationText(designer: Designer): string {
- if (designer.status === 'available' && (designer.idleDays ?? 0) >= 5) return '强烈推荐';
- if (designer.status === 'available') return '推荐';
- if (designer.status === 'busy') return '谨慎分配';
- return '不推荐';
- }
- // 统计方法
- getDesignerCountByStatus(status: string): number {
- return this.filteredDesigners.filter(d => d.status === status).length;
- }
- getStagnantProjectsCount(): number {
- return this.filteredDesigners
- .filter(d => d.status === 'stagnant')
- .reduce((sum, d) => sum + d.currentProjects, 0);
- }
- // 新增:统计今日的按日可用性数量
- getAvailabilityCount(level: 'free' | 'partial' | 'booked'): number {
- return this.filteredDesigners.filter(d => this.getAvailabilityLevel(d, this.currentDate) === level).length;
- }
- // 操作方法
- refreshData() {
- // 刷新数据逻辑
- this.applyFilters();
- }
- viewDesignerDetail(designer: Designer) {
- this.designerSelected.emit(designer);
- }
- assignToDesigner(designer: Designer) {
- this.assignmentRequested.emit(designer);
- }
- // 头像加载失败回退
- onAvatarError(event: Event) {
- const img = event.target as HTMLImageElement;
- img.src = this.defaultAvatarPath;
- }
- // ========== 月日历视图相关方法 ==========
-
- // 判断是否是今天
- isToday(date: Date): boolean {
- const today = new Date();
- return date.getDate() === today.getDate() &&
- date.getMonth() === today.getMonth() &&
- date.getFullYear() === today.getFullYear();
- }
- // 判断是否是当前月份
- isCurrentMonth(date: Date): boolean {
- return date.getMonth() === this.currentDate.getMonth() &&
- date.getFullYear() === this.currentDate.getFullYear();
- }
- // 判断是否是周末
- isWeekend(date: Date): boolean {
- const day = date.getDay();
- return day === 0 || day === 6;
- }
- // 获取设计师在指定日期的状态CSS类
- getDesignerDayStatusClass(designer: Designer, date: Date): string {
- const classes: string[] = [];
-
- if (this.isDateAvailable(designer, date)) {
- classes.push('available');
- }
- if (this.isDateReview(designer, date)) {
- classes.push('review');
- }
- if (this.isDateBusy(designer, date)) {
- classes.push('busy');
- }
-
- return classes.join(' ');
- }
- // 获取设计师在指定日期的状态标题
- getDesignerDayStatusTitle(designer: Designer, date: Date): string {
- const statuses: string[] = [];
-
- statuses.push(`${designer.name}`);
-
- if (this.isDateAvailable(designer, date)) {
- statuses.push('空闲可接单');
- }
- if (this.isDateReview(designer, date)) {
- statuses.push('对图日');
- }
- if (this.isDateBusy(designer, date)) {
- statuses.push('忙碌中');
- }
- const events = this.getDateEvents(designer, date);
- if (events.length > 0) {
- statuses.push(`${events.length}个事件`);
- }
- return statuses.join(' · ');
- }
- // 判断设计师在指定日期是否空闲
- isDateAvailable(designer: Designer, date: Date): boolean {
- // 如果设计师状态是available且该日期没有事件,则认为空闲
- if (designer.status === 'available') {
- const events = this.getDateEvents(designer, date);
- return events.length === 0;
- }
- return false;
- }
- // 判断设计师在指定日期是否对图
- isDateReview(designer: Designer, date: Date): boolean {
- const dateStr = this.formatDateString(date);
- // 从upcomingEvents中查找对图事件
- const events = designer.upcomingEvents || [];
- return events.some(e => e.type === 'review' && this.formatDateString(e.date) === dateStr);
- }
- // 判断设计师在指定日期是否忙碌
- isDateBusy(designer: Designer, date: Date): boolean {
- const events = this.getDateEvents(designer, date);
- return events.some(e => e.type === 'project');
- }
- // 格式化日期为字符串(用于比较)
- formatDateString(date: Date): string {
- const year = date.getFullYear();
- const month = (date.getMonth() + 1).toString().padStart(2, '0');
- const day = date.getDate().toString().padStart(2, '0');
- return `${year}-${month}-${day}`;
- }
- // 获取工作量CSS类
- getWorkloadClass(workload: number): string {
- if (workload >= 80) return 'high';
- if (workload >= 50) return 'medium';
- return 'low';
- }
- }
|