|
@@ -1,498 +1,401 @@
|
|
|
-import { Component, OnInit, signal, computed, Inject } from '@angular/core';
|
|
|
|
|
|
|
+import { Component, OnInit, signal, inject } from '@angular/core';
|
|
|
import { CommonModule } from '@angular/common';
|
|
import { CommonModule } from '@angular/common';
|
|
|
-import { FormsModule } from '@angular/forms';
|
|
|
|
|
-import { MatButtonModule } from '@angular/material/button';
|
|
|
|
|
|
|
+import { MatTabsModule } from '@angular/material/tabs';
|
|
|
import { MatCardModule } from '@angular/material/card';
|
|
import { MatCardModule } from '@angular/material/card';
|
|
|
|
|
+import { MatButtonModule } from '@angular/material/button';
|
|
|
import { MatIconModule } from '@angular/material/icon';
|
|
import { MatIconModule } from '@angular/material/icon';
|
|
|
-import { MatDialogModule, MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
|
|
|
|
-import { MatTabsModule } from '@angular/material/tabs';
|
|
|
|
|
|
|
+import { MatChipsModule } from '@angular/material/chips';
|
|
|
|
|
+import { MatDividerModule } from '@angular/material/divider';
|
|
|
|
|
+import { MatProgressBarModule } from '@angular/material/progress-bar';
|
|
|
|
|
+import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
|
|
|
|
+import { MatTableModule } from '@angular/material/table';
|
|
|
|
|
+import { MatBadgeModule } from '@angular/material/badge';
|
|
|
|
|
+import { MatDialogModule, MatDialog } from '@angular/material/dialog';
|
|
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
|
-import { Employee, DesignerSkill, DesignerPortfolioItem } from '../../../models/hr.model';
|
|
|
|
|
|
|
+import { ActivatedRoute } from '@angular/router';
|
|
|
|
|
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
|
|
|
|
+
|
|
|
|
|
+// 模拟数据接口
|
|
|
|
|
+interface Designer {
|
|
|
|
|
+ id: string;
|
|
|
|
|
+ name: string;
|
|
|
|
|
+ avatar: string;
|
|
|
|
|
+ position: string;
|
|
|
|
|
+ level: string;
|
|
|
|
|
+ department: string;
|
|
|
|
|
+ joinDate: string;
|
|
|
|
|
+ skills: string[];
|
|
|
|
|
+ styles: string[];
|
|
|
|
|
+ availableDates: string[];
|
|
|
|
|
+ certificates: {
|
|
|
|
|
+ name: string;
|
|
|
|
|
+ date: string;
|
|
|
|
|
+ type: string;
|
|
|
|
|
+ }[];
|
|
|
|
|
+ projects: {
|
|
|
|
|
+ id: string;
|
|
|
|
|
+ name: string;
|
|
|
|
|
+ clientType: string;
|
|
|
|
|
+ role: string;
|
|
|
|
|
+ deliveryDate: string;
|
|
|
|
|
+ clientRating: number;
|
|
|
|
|
+ tags: string[];
|
|
|
|
|
+ feedback: string;
|
|
|
|
|
+ }[];
|
|
|
|
|
+ performance: {
|
|
|
|
|
+ quarter: string;
|
|
|
|
|
+ customerSatisfaction: number;
|
|
|
|
|
+ excellentWorkRate: number;
|
|
|
|
|
+ performanceValue: number;
|
|
|
|
|
+ deductions: {
|
|
|
|
|
+ reason: string;
|
|
|
|
|
+ points: number;
|
|
|
|
|
+ date: string;
|
|
|
|
|
+ }[];
|
|
|
|
|
+ finalGrade: string;
|
|
|
|
|
+ }[];
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
// 作品集预览对话框组件
|
|
// 作品集预览对话框组件
|
|
|
@Component({
|
|
@Component({
|
|
|
- selector: 'app-portfolio-preview-dialog',
|
|
|
|
|
|
|
+ selector: 'portfolio-preview-dialog',
|
|
|
standalone: true,
|
|
standalone: true,
|
|
|
- imports: [
|
|
|
|
|
- CommonModule,
|
|
|
|
|
- MatButtonModule,
|
|
|
|
|
- MatIconModule
|
|
|
|
|
- ],
|
|
|
|
|
|
|
+ imports: [CommonModule, MatDialogModule, MatButtonModule, MatIconModule],
|
|
|
template: `
|
|
template: `
|
|
|
- <div class="dialog-header">
|
|
|
|
|
- <h2>{{ portfolioItem.title }}</h2>
|
|
|
|
|
- <button class="close-btn" (click)="dialogRef.close()">
|
|
|
|
|
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
|
|
- <line x1="18" y1="6" x2="6" y2="18"></line>
|
|
|
|
|
- <line x1="6" y1="6" x2="18" y2="18"></line>
|
|
|
|
|
- </svg>
|
|
|
|
|
- </button>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="dialog-content">
|
|
|
|
|
- <div class="portfolio-image-container">
|
|
|
|
|
- <img [src]="portfolioItem.imageUrl" alt="{{ portfolioItem.title }}" class="portfolio-image">
|
|
|
|
|
- <button mat-icon-button class="nav-btn prev-btn" (click)="navigate(-1)" [disabled]="currentIndex === 0">
|
|
|
|
|
- <mat-icon>chevron_left</mat-icon>
|
|
|
|
|
- </button>
|
|
|
|
|
- <button mat-icon-button class="nav-btn next-btn" (click)="navigate(1)" [disabled]="currentIndex === portfolioItems.length - 1">
|
|
|
|
|
- <mat-icon>chevron_right</mat-icon>
|
|
|
|
|
|
|
+ <div class="portfolio-dialog">
|
|
|
|
|
+ <div class="dialog-header">
|
|
|
|
|
+ <h2>作品集预览</h2>
|
|
|
|
|
+ <button mat-icon-button (click)="close()">
|
|
|
|
|
+ <mat-icon>close</mat-icon>
|
|
|
</button>
|
|
</button>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="portfolio-details">
|
|
|
|
|
- <div class="detail-item">
|
|
|
|
|
- <label>项目名称:</label>
|
|
|
|
|
- <span>{{ portfolioItem.projectName || '无关联项目' }}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="detail-item">
|
|
|
|
|
- <label>完成日期:</label>
|
|
|
|
|
- <span>{{ formatDate(portfolioItem.completionDate) }}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="detail-item">
|
|
|
|
|
- <label>评分:</label>
|
|
|
|
|
- <div class="rating">
|
|
|
|
|
- <span *ngFor="let star of getStars()" class="star">{{ star }}</span>
|
|
|
|
|
- <span class="rating-number">{{ portfolioItem.rating }}/5</span>
|
|
|
|
|
|
|
+ <div class="dialog-content">
|
|
|
|
|
+ <div class="portfolio-gallery">
|
|
|
|
|
+ <div class="portfolio-item" *ngFor="let item of portfolioItems">
|
|
|
|
|
+ <img [src]="item.imageUrl" [alt]="item.title">
|
|
|
|
|
+ <div class="portfolio-info">
|
|
|
|
|
+ <h3>{{item.title}}</h3>
|
|
|
|
|
+ <p>{{item.description}}</p>
|
|
|
|
|
+ <div class="portfolio-tags">
|
|
|
|
|
+ <span *ngFor="let tag of item.tags">{{tag}}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="detail-item">
|
|
|
|
|
- <label>描述:</label>
|
|
|
|
|
- <p>{{ portfolioItem.description }}</p>
|
|
|
|
|
- </div>
|
|
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
`,
|
|
`,
|
|
|
styles: [`
|
|
styles: [`
|
|
|
|
|
+ .portfolio-dialog {
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+ max-width: 900px;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ }
|
|
|
.dialog-header {
|
|
.dialog-header {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
- margin-bottom: 24px;
|
|
|
|
|
- padding-bottom: 16px;
|
|
|
|
|
- border-bottom: 1px solid #e5e7eb;
|
|
|
|
|
|
|
+ padding: 16px 24px;
|
|
|
|
|
+ border-bottom: 1px solid #eee;
|
|
|
}
|
|
}
|
|
|
- .close-btn {
|
|
|
|
|
- background: none;
|
|
|
|
|
- border: none;
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- color: #6b7280;
|
|
|
|
|
- padding: 4px;
|
|
|
|
|
|
|
+ .dialog-header h2 {
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ font-size: 20px;
|
|
|
|
|
+ color: #1a3a6e;
|
|
|
}
|
|
}
|
|
|
.dialog-content {
|
|
.dialog-content {
|
|
|
- max-width: 90vw;
|
|
|
|
|
- max-height: 80vh;
|
|
|
|
|
|
|
+ padding: 24px;
|
|
|
|
|
+ max-height: 70vh;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
}
|
|
}
|
|
|
- .portfolio-image-container {
|
|
|
|
|
- position: relative;
|
|
|
|
|
- margin-bottom: 24px;
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
- border-radius: 8px;
|
|
|
|
|
- max-height: 500px;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
|
|
+ .portfolio-gallery {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
|
|
|
+ gap: 24px;
|
|
|
}
|
|
}
|
|
|
- .portfolio-image {
|
|
|
|
|
- max-width: 100%;
|
|
|
|
|
- max-height: 500px;
|
|
|
|
|
- object-fit: contain;
|
|
|
|
|
|
|
+ .portfolio-item {
|
|
|
border-radius: 8px;
|
|
border-radius: 8px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
|
|
|
|
+ transition: transform 0.3s;
|
|
|
}
|
|
}
|
|
|
- .nav-btn {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- top: 50%;
|
|
|
|
|
- transform: translateY(-50%);
|
|
|
|
|
- background-color: rgba(0, 0, 0, 0.5);
|
|
|
|
|
- color: white;
|
|
|
|
|
- width: 40px;
|
|
|
|
|
- height: 40px;
|
|
|
|
|
- border-radius: 50%;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- z-index: 10;
|
|
|
|
|
- }
|
|
|
|
|
- .prev-btn {
|
|
|
|
|
- left: 10px;
|
|
|
|
|
|
|
+ .portfolio-item:hover {
|
|
|
|
|
+ transform: translateY(-5px);
|
|
|
}
|
|
}
|
|
|
- .next-btn {
|
|
|
|
|
- right: 10px;
|
|
|
|
|
|
|
+ .portfolio-item img {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 180px;
|
|
|
|
|
+ object-fit: cover;
|
|
|
}
|
|
}
|
|
|
- .portfolio-details {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- gap: 16px;
|
|
|
|
|
|
|
+ .portfolio-info {
|
|
|
|
|
+ padding: 16px;
|
|
|
}
|
|
}
|
|
|
- .detail-item {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- gap: 4px;
|
|
|
|
|
|
|
+ .portfolio-info h3 {
|
|
|
|
|
+ margin: 0 0 8px;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ color: #1a3a6e;
|
|
|
}
|
|
}
|
|
|
- label {
|
|
|
|
|
- font-weight: 500;
|
|
|
|
|
- color: #374151;
|
|
|
|
|
|
|
+ .portfolio-info p {
|
|
|
|
|
+ margin: 0 0 12px;
|
|
|
font-size: 14px;
|
|
font-size: 14px;
|
|
|
|
|
+ color: #666;
|
|
|
}
|
|
}
|
|
|
- span {
|
|
|
|
|
- color: #4b5563;
|
|
|
|
|
- font-size: 16px;
|
|
|
|
|
- }
|
|
|
|
|
- .rating {
|
|
|
|
|
|
|
+ .portfolio-tags {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
- align-items: center;
|
|
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
gap: 8px;
|
|
gap: 8px;
|
|
|
}
|
|
}
|
|
|
- .star {
|
|
|
|
|
- color: #f59e0b;
|
|
|
|
|
- font-size: 16px;
|
|
|
|
|
- }
|
|
|
|
|
- .rating-number {
|
|
|
|
|
- font-size: 14px;
|
|
|
|
|
- color: #6b7280;
|
|
|
|
|
- }
|
|
|
|
|
- p {
|
|
|
|
|
- color: #4b5563;
|
|
|
|
|
- line-height: 1.6;
|
|
|
|
|
|
|
+ .portfolio-tags span {
|
|
|
|
|
+ background-color: #e6f7ff;
|
|
|
|
|
+ color: #1a3a6e;
|
|
|
|
|
+ padding: 4px 8px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
}
|
|
}
|
|
|
`]
|
|
`]
|
|
|
-}) class PortfolioPreviewDialog {
|
|
|
|
|
- portfolioItem: DesignerPortfolioItem;
|
|
|
|
|
- portfolioItems: DesignerPortfolioItem[];
|
|
|
|
|
- currentIndex: number;
|
|
|
|
|
-
|
|
|
|
|
- constructor(
|
|
|
|
|
- public dialogRef: MatDialogRef<PortfolioPreviewDialog>,
|
|
|
|
|
- @Inject(MAT_DIALOG_DATA) public data: any
|
|
|
|
|
- ) {
|
|
|
|
|
- this.portfolioItem = data.portfolioItem;
|
|
|
|
|
- this.portfolioItems = data.portfolioItems;
|
|
|
|
|
- this.currentIndex = this.portfolioItems.findIndex(item => item.id === this.portfolioItem.id);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- formatDate(date: Date): string {
|
|
|
|
|
- if (!date) return '';
|
|
|
|
|
- const d = new Date(date);
|
|
|
|
|
- return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- getStars() {
|
|
|
|
|
- const stars = [];
|
|
|
|
|
- for (let i = 1; i <= 5; i++) {
|
|
|
|
|
- stars.push(i <= this.portfolioItem.rating ? '★' : '☆');
|
|
|
|
|
- }
|
|
|
|
|
- return stars;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- navigate(direction: number) {
|
|
|
|
|
- this.currentIndex += direction;
|
|
|
|
|
- this.portfolioItem = this.portfolioItems[this.currentIndex];
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// 生成模拟设计师数据
|
|
|
|
|
-const generateMockDesigner = (): Employee => {
|
|
|
|
|
- return {
|
|
|
|
|
- id: 'emp-design-001',
|
|
|
|
|
- name: '张三设计师',
|
|
|
|
|
- department: '设计部',
|
|
|
|
|
- position: '高级设计师',
|
|
|
|
|
- employeeId: 'EMP2020001',
|
|
|
|
|
- phone: '13812345678',
|
|
|
|
|
- email: 'zhang@example.com',
|
|
|
|
|
- gender: '男',
|
|
|
|
|
- birthDate: new Date(1990, 5, 15),
|
|
|
|
|
- hireDate: new Date(2020, 3, 1),
|
|
|
|
|
- status: '在职',
|
|
|
|
|
- avatar: 'https://via.placeholder.com/120x120/CCCCFF/555555?text=张'
|
|
|
|
|
- };
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-// 生成模拟设计师技能数据
|
|
|
|
|
-const generateMockSkills = (): DesignerSkill[] => {
|
|
|
|
|
- const skills = [
|
|
|
|
|
- { id: 'skill-001', name: '3D建模', level: 5 },
|
|
|
|
|
- { id: 'skill-002', name: '渲染', level: 5 },
|
|
|
|
|
- { id: 'skill-003', name: '空间设计', level: 4 },
|
|
|
|
|
- { id: 'skill-004', name: '色彩搭配', level: 5 },
|
|
|
|
|
- { id: 'skill-005', name: 'CAD绘图', level: 4 },
|
|
|
|
|
- { id: 'skill-006', name: '客户沟通', level: 3 },
|
|
|
|
|
- { id: 'skill-007', name: '项目管理', level: 3 }
|
|
|
|
|
- ];
|
|
|
|
|
- return skills;
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-// 生成模拟作品集数据
|
|
|
|
|
-const generateMockPortfolio = (): DesignerPortfolioItem[] => {
|
|
|
|
|
- const portfolio: DesignerPortfolioItem[] = [];
|
|
|
|
|
- const projectNames = ['现代风格客厅设计', '欧式厨房改造', '极简卧室设计', '办公室规划', '新中式书房设计', '北欧风卫生间改造', '工业风loft设计', '日式庭院景观'];
|
|
|
|
|
- const descriptions = [
|
|
|
|
|
- '采用简约现代风格,强调功能性与美学的平衡,通过中性色调与自然材质创造舒适空间。',
|
|
|
|
|
- '融合古典欧式元素与现代厨房功能需求,营造优雅而实用的烹饪环境。',
|
|
|
|
|
- '以极简主义为核心,通过留白、线条和光影创造宁静舒适的休息空间。',
|
|
|
|
|
- '优化办公空间布局,提高团队协作效率,同时关注员工舒适度与健康。',
|
|
|
|
|
- '结合传统中式元素与现代设计语言,打造兼具文化韵味与现代感的阅读空间。',
|
|
|
|
|
- '北欧风格注重自然光线与功能性,创造明亮、干净、温馨的卫生间环境。',
|
|
|
|
|
- '工业风强调原始结构与材质的暴露,创造粗犷而不失精致的loft空间。',
|
|
|
|
|
- '日式庭院设计注重自然和谐,通过枯山水、绿植和石灯笼等元素营造宁静氛围。'
|
|
|
|
|
- ];
|
|
|
|
|
-
|
|
|
|
|
- for (let i = 1; i <= 8; i++) {
|
|
|
|
|
- const completionDate = new Date();
|
|
|
|
|
- completionDate.setMonth(completionDate.getMonth() - i);
|
|
|
|
|
-
|
|
|
|
|
- portfolio.push({
|
|
|
|
|
- id: `portfolio-${i}`,
|
|
|
|
|
- title: projectNames[i % projectNames.length],
|
|
|
|
|
- description: descriptions[i % descriptions.length],
|
|
|
|
|
- imageUrl: `https://via.placeholder.com/600x400/${Math.floor(Math.random()*16777215).toString(16).padStart(6, '0')}/FFFFFF?text=作品${i}`,
|
|
|
|
|
- projectId: `proj-${i}`,
|
|
|
|
|
- projectName: projectNames[i % projectNames.length],
|
|
|
|
|
- completionDate,
|
|
|
|
|
- rating: Math.floor(Math.random() * 2) + 4 // 4-5星评分
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return portfolio;
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-// 生成模拟项目历史数据
|
|
|
|
|
-const generateMockProjectHistory = () => {
|
|
|
|
|
- const projects = [
|
|
|
|
|
|
|
+})
|
|
|
|
|
+export class PortfolioPreviewDialog {
|
|
|
|
|
+ portfolioItems = [
|
|
|
{
|
|
{
|
|
|
- id: 'proj-history-001',
|
|
|
|
|
- name: '现代风格客厅设计',
|
|
|
|
|
- customerName: '李四',
|
|
|
|
|
- startDate: new Date(2023, 1, 10),
|
|
|
|
|
- endDate: new Date(2023, 3, 15),
|
|
|
|
|
- status: '已完成',
|
|
|
|
|
- rating: 5,
|
|
|
|
|
- role: '主设计师',
|
|
|
|
|
- skillsUsed: ['3D建模', '渲染', '空间设计'],
|
|
|
|
|
- feedback: '设计效果非常满意,完全符合我的预期,沟通也很顺畅。'
|
|
|
|
|
|
|
+ imageUrl: 'assets/images/portfolio-1.svg',
|
|
|
|
|
+ title: '现代简约风客厅',
|
|
|
|
|
+ description: '为装修公司客户打造的现代简约风格客厅效果图',
|
|
|
|
|
+ tags: ['客厅', '现代简约', '3D建模']
|
|
|
},
|
|
},
|
|
|
{
|
|
{
|
|
|
- id: 'proj-history-002',
|
|
|
|
|
- name: '欧式厨房改造',
|
|
|
|
|
- customerName: '王五',
|
|
|
|
|
- startDate: new Date(2023, 4, 5),
|
|
|
|
|
- endDate: new Date(2023, 6, 10),
|
|
|
|
|
- status: '已完成',
|
|
|
|
|
- rating: 4,
|
|
|
|
|
- role: '主设计师',
|
|
|
|
|
- skillsUsed: ['3D建模', '渲染', '色彩搭配'],
|
|
|
|
|
- feedback: '设计很专业,细节处理到位,就是交付时间稍微延迟了一点。'
|
|
|
|
|
|
|
+ imageUrl: 'assets/images/portfolio-2.svg',
|
|
|
|
|
+ title: '北欧风卧室',
|
|
|
|
|
+ description: '设计工作室委托的北欧风格卧室设计',
|
|
|
|
|
+ tags: ['卧室', '北欧风', '软装搭配']
|
|
|
},
|
|
},
|
|
|
{
|
|
{
|
|
|
- id: 'proj-history-003',
|
|
|
|
|
- name: '极简卧室设计',
|
|
|
|
|
- customerName: '赵六',
|
|
|
|
|
- startDate: new Date(2023, 7, 20),
|
|
|
|
|
- endDate: new Date(2023, 9, 25),
|
|
|
|
|
- status: '已完成',
|
|
|
|
|
- rating: 5,
|
|
|
|
|
- role: '主设计师',
|
|
|
|
|
- skillsUsed: ['空间设计', '色彩搭配', 'CAD绘图'],
|
|
|
|
|
- feedback: '完美的极简风格,我非常喜欢,推荐给了我的朋友们。'
|
|
|
|
|
|
|
+ imageUrl: 'assets/images/portfolio-3.svg',
|
|
|
|
|
+ title: '日式风格书房',
|
|
|
|
|
+ description: '个人客户定制的日式风格书房设计',
|
|
|
|
|
+ tags: ['书房', '日式', '全屋定制']
|
|
|
},
|
|
},
|
|
|
{
|
|
{
|
|
|
- id: 'proj-history-004',
|
|
|
|
|
- name: '办公室规划',
|
|
|
|
|
- customerName: '钱七',
|
|
|
|
|
- startDate: new Date(2023, 10, 1),
|
|
|
|
|
- endDate: null,
|
|
|
|
|
- status: '进行中',
|
|
|
|
|
- rating: null,
|
|
|
|
|
- role: '设计顾问',
|
|
|
|
|
- skillsUsed: ['空间设计', '项目管理', '客户沟通'],
|
|
|
|
|
- feedback: null
|
|
|
|
|
|
|
+ imageUrl: 'assets/images/portfolio-4.svg',
|
|
|
|
|
+ title: '工业风餐厅',
|
|
|
|
|
+ description: '商业空间的工业风格餐厅设计',
|
|
|
|
|
+ tags: ['餐厅', '工业风', '商业空间']
|
|
|
}
|
|
}
|
|
|
];
|
|
];
|
|
|
- return projects;
|
|
|
|
|
-};
|
|
|
|
|
|
|
|
|
|
-// 生成擅长领域数据
|
|
|
|
|
-const generateMockSpecialties = () => {
|
|
|
|
|
- return [
|
|
|
|
|
- { name: '现代风格', count: 15, level: 5 },
|
|
|
|
|
- { name: '极简主义', count: 12, level: 5 },
|
|
|
|
|
- { name: '北欧风', count: 8, level: 4 },
|
|
|
|
|
- { name: '工业风', count: 6, level: 4 },
|
|
|
|
|
- { name: '新中式', count: 5, level: 3 },
|
|
|
|
|
- { name: '欧式', count: 7, level: 4 }
|
|
|
|
|
- ];
|
|
|
|
|
-};
|
|
|
|
|
|
|
+ constructor(private dialog: MatDialog) {}
|
|
|
|
|
+
|
|
|
|
|
+ close() {
|
|
|
|
|
+ this.dialog.closeAll();
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
-// 主组件
|
|
|
|
|
@Component({
|
|
@Component({
|
|
|
selector: 'app-designer-profile',
|
|
selector: 'app-designer-profile',
|
|
|
standalone: true,
|
|
standalone: true,
|
|
|
imports: [
|
|
imports: [
|
|
|
CommonModule,
|
|
CommonModule,
|
|
|
- FormsModule,
|
|
|
|
|
- MatButtonModule,
|
|
|
|
|
|
|
+ MatTabsModule,
|
|
|
MatCardModule,
|
|
MatCardModule,
|
|
|
|
|
+ MatButtonModule,
|
|
|
MatIconModule,
|
|
MatIconModule,
|
|
|
|
|
+ MatChipsModule,
|
|
|
|
|
+ MatDividerModule,
|
|
|
|
|
+ MatProgressBarModule,
|
|
|
|
|
+ MatProgressSpinnerModule,
|
|
|
|
|
+ MatTableModule,
|
|
|
|
|
+ MatBadgeModule,
|
|
|
MatDialogModule,
|
|
MatDialogModule,
|
|
|
- MatTabsModule,
|
|
|
|
|
- MatTooltipModule
|
|
|
|
|
|
|
+ MatTooltipModule,
|
|
|
|
|
+ FormsModule,
|
|
|
|
|
+ ReactiveFormsModule
|
|
|
],
|
|
],
|
|
|
templateUrl: './designer-profile.html',
|
|
templateUrl: './designer-profile.html',
|
|
|
- styleUrl: './designer-profile.scss'
|
|
|
|
|
-}) export class DesignerProfile implements OnInit {
|
|
|
|
|
- // 暴露Math对象给模板使用
|
|
|
|
|
- readonly Math = Math;
|
|
|
|
|
-
|
|
|
|
|
- // 数据
|
|
|
|
|
- designer = signal<Employee>({} as Employee);
|
|
|
|
|
- skills = signal<DesignerSkill[]>([]);
|
|
|
|
|
- portfolio = signal<DesignerPortfolioItem[]>([]);
|
|
|
|
|
- projectHistory = signal<any[]>([]);
|
|
|
|
|
- specialties = signal<any[]>([]);
|
|
|
|
|
- selectedPortfolioItem = signal<DesignerPortfolioItem | null>(null);
|
|
|
|
|
- searchTerm = signal('');
|
|
|
|
|
- selectedSkills = signal<string[]>([]);
|
|
|
|
|
- activeTab = signal('overview');
|
|
|
|
|
-
|
|
|
|
|
- // 计算属性
|
|
|
|
|
- orderedProjectHistory = computed(() => {
|
|
|
|
|
- return [...this.projectHistory()].sort((a, b) =>
|
|
|
|
|
- new Date(b.startDate).getTime() - new Date(a.startDate).getTime()
|
|
|
|
|
- );
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- inProgressProjects = computed(() => {
|
|
|
|
|
- return this.projectHistory().filter(p => p.status === '进行中');
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- // 获取星级评分显示
|
|
|
|
|
- getStars(rating: number): string[] {
|
|
|
|
|
- const stars: string[] = [];
|
|
|
|
|
- const numRating = parseFloat(rating.toString()); // 确保是数字类型
|
|
|
|
|
- for (let i = 1; i <= 5; i++) {
|
|
|
|
|
- stars.push(i <= numRating ? '★' : '☆');
|
|
|
|
|
- }
|
|
|
|
|
- return stars;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- filteredPortfolio = computed(() => {
|
|
|
|
|
- let filtered = this.portfolio();
|
|
|
|
|
-
|
|
|
|
|
- // 按技能筛选
|
|
|
|
|
- if (this.selectedSkills().length > 0) {
|
|
|
|
|
- // 在实际应用中,这里应该根据作品关联的技能进行筛选
|
|
|
|
|
- // 这里使用随机筛选来模拟效果
|
|
|
|
|
- filtered = filtered.filter(() => Math.random() > 0.3);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 按搜索词筛选
|
|
|
|
|
- if (this.searchTerm()) {
|
|
|
|
|
- const term = this.searchTerm().toLowerCase();
|
|
|
|
|
- filtered = filtered.filter(item =>
|
|
|
|
|
- item.title.toLowerCase().includes(term) ||
|
|
|
|
|
- item.description.toLowerCase().includes(term) ||
|
|
|
|
|
- item.projectName?.toLowerCase().includes(term)
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return filtered;
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- // 评分统计
|
|
|
|
|
- ratingStats = computed(() => {
|
|
|
|
|
- const completedProjects = this.projectHistory().filter(p => p.status === '已完成');
|
|
|
|
|
- const totalRating = completedProjects.reduce((sum, project) => sum + project.rating, 0);
|
|
|
|
|
- const avgRating = completedProjects.length > 0 ? totalRating / completedProjects.length : 0;
|
|
|
|
|
-
|
|
|
|
|
- return {
|
|
|
|
|
- totalProjects: completedProjects.length,
|
|
|
|
|
- avgRating,
|
|
|
|
|
- fiveStarCount: completedProjects.filter(p => p.rating === 5).length,
|
|
|
|
|
- fourStarCount: completedProjects.filter(p => p.rating === 4).length,
|
|
|
|
|
- threeStarCount: completedProjects.filter(p => p.rating === 3).length
|
|
|
|
|
- };
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- constructor(private dialog: MatDialog) {}
|
|
|
|
|
|
|
+ styleUrls: ['./designer-profile.scss']
|
|
|
|
|
+})
|
|
|
|
|
+export class DesignerProfile implements OnInit {
|
|
|
|
|
+ designer = signal<Designer | null>(null);
|
|
|
|
|
+ activeTab = signal(0);
|
|
|
|
|
+ projectColumns = ['name', 'clientType', 'role', 'deliveryDate', 'clientRating', 'actions'];
|
|
|
|
|
+ performanceColumns = ['quarter', 'customerSatisfaction', 'excellentWorkRate', 'performanceValue', 'finalGrade', 'actions'];
|
|
|
|
|
+ isLoading = signal(true);
|
|
|
|
|
+ projectsLoading = signal(true);
|
|
|
|
|
+ performanceLoading = signal(true);
|
|
|
|
|
|
|
|
|
|
+ private route = inject(ActivatedRoute);
|
|
|
|
|
+ private dialog = inject(MatDialog);
|
|
|
|
|
+
|
|
|
|
|
+ constructor() {}
|
|
|
|
|
+
|
|
|
ngOnInit() {
|
|
ngOnInit() {
|
|
|
- // 加载模拟数据
|
|
|
|
|
- this.designer.set(generateMockDesigner());
|
|
|
|
|
- this.skills.set(generateMockSkills());
|
|
|
|
|
- this.portfolio.set(generateMockPortfolio());
|
|
|
|
|
- this.projectHistory.set(generateMockProjectHistory());
|
|
|
|
|
- this.specialties.set(generateMockSpecialties());
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 切换标签页
|
|
|
|
|
- switchTab(tab: string) {
|
|
|
|
|
- this.activeTab.set(tab);
|
|
|
|
|
|
|
+ // 从路由参数获取设计师ID
|
|
|
|
|
+ this.route.paramMap.subscribe(params => {
|
|
|
|
|
+ const id = params.get('id');
|
|
|
|
|
+ if (id) {
|
|
|
|
|
+ this.loadDesignerData(id);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 模拟加载项目数据
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ this.projectsLoading.set(false);
|
|
|
|
|
+ }, 1500);
|
|
|
|
|
+
|
|
|
|
|
+ // 模拟加载绩效数据和初始化性能趋势图表数据
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ this.performanceLoading.set(false);
|
|
|
|
|
+ const performanceData = this.generatePerformanceTrend();
|
|
|
|
|
+ console.log('Performance trend data:', performanceData);
|
|
|
|
|
+ // 这里可以添加图表初始化代码
|
|
|
|
|
+ }, 2000);
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // 格式化日期
|
|
|
|
|
- formatDate(date: Date | string | null): string {
|
|
|
|
|
- if (!date) return '';
|
|
|
|
|
- const d = new Date(date);
|
|
|
|
|
- return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
|
|
|
|
|
+
|
|
|
|
|
+ loadDesignerData(id: string) {
|
|
|
|
|
+ // 模拟API调用加载设计师数据
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ this.designer.set({
|
|
|
|
|
+ id,
|
|
|
|
|
+ name: '张明',
|
|
|
|
|
+ avatar: 'assets/images/default-avatar.svg',
|
|
|
|
|
+ position: '高级设计师',
|
|
|
|
|
+ level: '高级',
|
|
|
|
|
+ department: '设计部-效果图组',
|
|
|
|
|
+ joinDate: '2020-05-15',
|
|
|
|
|
+ skills: ['3D建模', '渲染', '后期处理', 'CAD制图'],
|
|
|
|
|
+ styles: ['现代简约', '北欧风格', '新中式'],
|
|
|
|
|
+ availableDates: ['2023-06-15', '2023-06-16', '2023-06-17'],
|
|
|
|
|
+ certificates: [
|
|
|
|
|
+ { name: '3D效果图高级设计师认证', date: '2021-03-10', type: '技能认证' },
|
|
|
|
|
+ { name: '优秀设计师季度奖', date: '2022-12-20', type: '荣誉证书' }
|
|
|
|
|
+ ],
|
|
|
|
|
+ projects: [
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'P001',
|
|
|
|
|
+ name: '城市花园小区-89平米-现代简约',
|
|
|
|
|
+ clientType: '装修公司',
|
|
|
|
|
+ role: '效果图设计',
|
|
|
|
|
+ deliveryDate: '2023-05-20',
|
|
|
|
|
+ clientRating: 4.8,
|
|
|
|
|
+ tags: ['住宅', '现代简约'],
|
|
|
|
|
+ feedback: '设计师理解需求准确,效果图质量高,客户非常满意。'
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'P002',
|
|
|
|
|
+ name: '星河湾别墅-280平米-新中式',
|
|
|
|
|
+ clientType: '设计工作室',
|
|
|
|
|
+ role: '建模+渲染',
|
|
|
|
|
+ deliveryDate: '2023-04-15',
|
|
|
|
|
+ clientRating: 4.5,
|
|
|
|
|
+ tags: ['别墅', '新中式'],
|
|
|
|
|
+ feedback: '建模细节处理到位,渲染效果自然,符合预期。'
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'P003',
|
|
|
|
|
+ name: '商业广场-咖啡厅设计',
|
|
|
|
|
+ clientType: '装修公司',
|
|
|
|
|
+ role: '全案设计',
|
|
|
|
|
+ deliveryDate: '2023-03-10',
|
|
|
|
|
+ clientRating: 5.0,
|
|
|
|
|
+ tags: ['商业空间', '工业风'],
|
|
|
|
|
+ feedback: '从建模到后期一条龙服务,效率高,质量好,客户非常满意。'
|
|
|
|
|
+ }
|
|
|
|
|
+ ],
|
|
|
|
|
+ performance: [
|
|
|
|
|
+ {
|
|
|
|
|
+ quarter: '2023-Q1',
|
|
|
|
|
+ customerSatisfaction: 92,
|
|
|
|
|
+ excellentWorkRate: 85,
|
|
|
|
|
+ performanceValue: 88,
|
|
|
|
|
+ deductions: [
|
|
|
|
|
+ { reason: '项目延期', points: 3, date: '2023-02-15' }
|
|
|
|
|
+ ],
|
|
|
|
|
+ finalGrade: 'A'
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ quarter: '2022-Q4',
|
|
|
|
|
+ customerSatisfaction: 88,
|
|
|
|
|
+ excellentWorkRate: 80,
|
|
|
|
|
+ performanceValue: 85,
|
|
|
|
|
+ deductions: [],
|
|
|
|
|
+ finalGrade: 'A-'
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ quarter: '2022-Q3',
|
|
|
|
|
+ customerSatisfaction: 85,
|
|
|
|
|
+ excellentWorkRate: 78,
|
|
|
|
|
+ performanceValue: 82,
|
|
|
|
|
+ deductions: [
|
|
|
|
|
+ { reason: '客户投诉', points: 5, date: '2022-08-10' }
|
|
|
|
|
+ ],
|
|
|
|
|
+ finalGrade: 'B+'
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ });
|
|
|
|
|
+ }, 500);
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // 打开作品集预览对话框
|
|
|
|
|
- openPortfolioPreview(item: DesignerPortfolioItem) {
|
|
|
|
|
|
|
+
|
|
|
|
|
+ openPortfolioPreview() {
|
|
|
this.dialog.open(PortfolioPreviewDialog, {
|
|
this.dialog.open(PortfolioPreviewDialog, {
|
|
|
- width: '90vw',
|
|
|
|
|
|
|
+ width: '80%',
|
|
|
maxWidth: '1200px',
|
|
maxWidth: '1200px',
|
|
|
- maxHeight: '90vh',
|
|
|
|
|
- data: {
|
|
|
|
|
- portfolioItem: item,
|
|
|
|
|
- portfolioItems: this.portfolio()
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ panelClass: 'portfolio-dialog-container'
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // 切换技能筛选
|
|
|
|
|
- toggleSkillFilter(skillName: string) {
|
|
|
|
|
- this.selectedSkills.update(skills => {
|
|
|
|
|
- if (skills.includes(skillName)) {
|
|
|
|
|
- return skills.filter(s => s !== skillName);
|
|
|
|
|
- } else {
|
|
|
|
|
- return [...skills, skillName];
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 获取技能等级对应的样式
|
|
|
|
|
- getSkillLevelClass(level: number): string {
|
|
|
|
|
- if (level >= 5) return 'level-expert';
|
|
|
|
|
- if (level >= 4) return 'level-advanced';
|
|
|
|
|
- if (level >= 3) return 'level-intermediate';
|
|
|
|
|
- return 'level-beginner';
|
|
|
|
|
|
|
+
|
|
|
|
|
+ getPerformanceTrend(metric: 'customerSatisfaction' | 'excellentWorkRate' | 'performanceValue'): string {
|
|
|
|
|
+ if (!this.designer() || this.designer()!.performance.length < 2) {
|
|
|
|
|
+ return 'stable';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const performance = this.designer()!.performance;
|
|
|
|
|
+ const current = performance[0][metric];
|
|
|
|
|
+ const previous = performance[1][metric];
|
|
|
|
|
+
|
|
|
|
|
+ if (current > previous + 3) return 'up';
|
|
|
|
|
+ if (current < previous - 3) return 'down';
|
|
|
|
|
+ return 'stable';
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // 获取项目状态样式
|
|
|
|
|
- getProjectStatusClass(status: string): string {
|
|
|
|
|
- switch (status) {
|
|
|
|
|
- case '已完成':
|
|
|
|
|
- return 'status-completed';
|
|
|
|
|
- case '进行中':
|
|
|
|
|
- return 'status-in-progress';
|
|
|
|
|
- default:
|
|
|
|
|
- return '';
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 性能趋势分析
|
|
|
|
|
+ generatePerformanceTrend() {
|
|
|
|
|
+ // 模拟生成过去12个月的性能数据
|
|
|
|
|
+ const months = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'];
|
|
|
|
|
+ const data = months.map(month => ({
|
|
|
|
|
+ month,
|
|
|
|
|
+ score: Math.floor(Math.random() * 30) + 70, // 70-100之间的随机分数
|
|
|
|
|
+ projects: Math.floor(Math.random() * 5) + 1, // 1-5之间的随机项目数
|
|
|
|
|
+ revenue: Math.floor(Math.random() * 10000) + 5000 // 5000-15000之间的随机收入
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
|
|
+ // 确保数据有一定的趋势性,而不是完全随机
|
|
|
|
|
+ for (let i = 1; i < data.length; i++) {
|
|
|
|
|
+ // 让分数有一定的连续性
|
|
|
|
|
+ data[i].score = Math.max(70, Math.min(100,
|
|
|
|
|
+ data[i-1].score + (Math.random() > 0.5 ? 1 : -1) * Math.floor(Math.random() * 5)));
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ return data;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
|
|
|
-
|
|
|
|
|
- // 查看客户评价详情
|
|
|
|
|
- viewFeedback(project: any) {
|
|
|
|
|
- alert(`客户反馈: ${project.feedback}`);
|
|
|
|
|
|
|
+ getPerformanceColor(grade: string): string {
|
|
|
|
|
+ if (grade.startsWith('A')) return '#52c41a';
|
|
|
|
|
+ if (grade.startsWith('B')) return '#1890ff';
|
|
|
|
|
+ if (grade.startsWith('C')) return '#faad14';
|
|
|
|
|
+ return '#f5222d';
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // 生成技能雷达图数据
|
|
|
|
|
- getRadarChartData() {
|
|
|
|
|
- // 在实际应用中,这里应该返回用于渲染雷达图的数据
|
|
|
|
|
- // 这里仅返回原始技能数据,图表渲染将在前端完成
|
|
|
|
|
- return this.skills().map(skill => ({
|
|
|
|
|
- subject: skill.name,
|
|
|
|
|
- A: skill.level,
|
|
|
|
|
- fullMark: 5
|
|
|
|
|
- }));
|
|
|
|
|
|
|
+
|
|
|
|
|
+ changeTab(index: number) {
|
|
|
|
|
+ this.activeTab.set(index);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ getRatingStars(rating: number): string[] {
|
|
|
|
|
+ const fullStars = Math.floor(rating);
|
|
|
|
|
+ const halfStar = rating % 1 >= 0.5;
|
|
|
|
|
+ const emptyStars = 5 - fullStars - (halfStar ? 1 : 0);
|
|
|
|
|
+
|
|
|
|
|
+ return [
|
|
|
|
|
+ ...Array(fullStars).fill('star'),
|
|
|
|
|
+ ...(halfStar ? ['star_half'] : []),
|
|
|
|
|
+ ...Array(emptyStars).fill('star_border')
|
|
|
|
|
+ ];
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|