Browse Source

feet:hr-and customer

徐福静0235668 4 hours ago
parent
commit
044b69e87b

+ 125 - 0
src/app/models/hr.model.ts

@@ -0,0 +1,125 @@
+// HR模块模型定义
+
+// 员工状态
+export type EmployeeStatus = '在职' | '离职' | '试用期';
+
+// 员工基本信息
+export interface Employee {
+  id: string;
+  name: string;
+  department: string;
+  position: string;
+  employeeId: string;
+  phone: string;
+  email: string;
+  gender: string;
+  birthDate: Date;
+  hireDate: Date;
+  status: EmployeeStatus;
+  avatar?: string;
+  contract?: Contract;
+  certificates?: Certificate[];
+}
+
+// 合同信息
+export interface Contract {
+  id: string;
+  startDate: Date;
+  endDate: Date;
+  type: string;
+  fileUrl?: string;
+  isExpiringSoon: boolean;
+}
+
+// 证件信息
+export interface Certificate {
+  id: string;
+  name: string;
+  type: string;
+  number: string;
+  issueDate: Date;
+  expiryDate?: Date;
+  fileUrl?: string;
+}
+
+// 考勤记录
+export interface Attendance {
+  id: string;
+  employeeId: string;
+  date: Date;
+  checkInTime?: Date;
+  checkOutTime?: Date;
+  status: '正常' | '迟到' | '早退' | '旷工' | '请假';
+  workHours: number;
+  projectId?: string;
+  projectName?: string;
+}
+
+// 资产类型
+export type AssetType = '电脑' | '外设' | '软件账号' | '域名' | '其他';
+
+// 资产状态
+export type AssetStatus = '空闲' | '占用' | '故障' | '报修中';
+
+// 资产信息
+export interface Asset {
+  id: string;
+  name: string;
+  type: AssetType;
+  status: AssetStatus;
+  purchaseDate: Date;
+  value: number;
+  assignedTo?: string;
+  assignedToName?: string;
+  department?: string;
+  description?: string;
+  serialNumber?: string;
+  warrantyExpiry?: Date;
+}
+
+// 资产分配记录
+export interface AssetAssignment {
+  id: string;
+  assetId: string;
+  employeeId: string;
+  startDate: Date;
+  endDate?: Date;
+  status: '进行中' | '已归还';
+}
+
+// 设计师技能
+export interface DesignerSkill {
+  id: string;
+  name: string;
+  level: number; // 1-5
+}
+
+// 设计师作品
+export interface DesignerPortfolioItem {
+  id: string;
+  title: string;
+  description: string;
+  imageUrl: string;
+  projectId?: string;
+  projectName?: string;
+  completionDate: Date;
+  rating: number; // 1-5
+}
+
+// 部门信息
+export interface Department {
+  id: string;
+  name: string;
+  managerId?: string;
+  managerName?: string;
+  employeeCount: number;
+}
+
+// 岗位信息
+export interface Position {
+  id: string;
+  name: string;
+  departmentId: string;
+  departmentName: string;
+  level: string;
+}

+ 35 - 4
src/app/pages/customer-service/customer-service-layout/customer-service-layout.html

@@ -18,8 +18,9 @@
         [(ngModel)]="searchTerm"
         placeholder="搜索项目、客户或动态..." 
         class="search-input"
+        (keyup.enter)="handleSearch()"
       />
-      <button class="search-button">
+      <button class="search-button" (click)="handleSearch()">
         <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor">
           <circle cx="11" cy="11" r="8"></circle>
           <path d="m21 21-4.35-4.35"></path>
@@ -29,12 +30,42 @@
   </div>
   
   <div class="navbar-right">
-    <button class="notification-btn">
+    <button class="notification-btn" (click)="toggleNotifications()">
       <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
         <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
       </svg>
-      <span class="notification-badge">3</span>
+      <span class="notification-badge">{{ notifications().length }}</span>
     </button>
+    
+    <!-- 通知下拉菜单 -->
+    <div class="notification-dropdown" *ngIf="showNotifications()">
+      <div class="notification-header">
+        <h3>消息通知</h3>
+        <button class="close-btn" (click)="toggleNotifications()">
+          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
+            <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="notification-list">
+        <div *ngFor="let notification of notifications()" class="notification-item">
+          <div class="notification-icon">
+            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
+              <circle cx="12" cy="12" r="10"></circle>
+              <line x1="12" y1="8" x2="12" y2="12"></line>
+              <line x1="12" y1="16" x2="12.01" y2="16"></line>
+            </svg>
+          </div>
+          <div class="notification-content">
+            <p>{{ notification }}</p>
+          </div>
+        </div>
+      </div>
+      <div class="notification-footer">
+        <a href="#" class="view-all">查看全部</a>
+      </div>
+    </div>
     <div class="user-profile">
       <img src="https://via.placeholder.com/40x40/FFCCCC/555555?text=CS" alt="用户头像" class="user-avatar">
       <span class="user-name">客服小李</span>
@@ -93,7 +124,7 @@
       <div class="storage-info">
         <span>在线时长: 4.5h</span>
       </div>
-      <button class="logout-btn">
+      <button class="logout-btn" (click)="logout()">
         <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
           <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
           <polyline points="16 17 21 12 16 7"></polyline>

+ 183 - 3
src/app/pages/customer-service/customer-service-layout/customer-service-layout.scss

@@ -183,17 +183,25 @@ $transition: all 0.3s ease;
       text-decoration: none;
       border-left: 3px solid transparent;
       transition: $transition;
+      position: relative;
 
       &:hover {
         background-color: $background-tertiary;
         color: $primary-color;
+        transform: translateX(2px);
       }
 
       &.active {
-        color: $primary-color;
-        background-color: color-mix(in srgb, $primary-color 5%, transparent);
+        color: white;
+        background-color: $primary-color;
         border-left-color: $primary-color;
-        font-weight: 500;
+        font-weight: 600;
+        box-shadow: 0 4px 8px color-mix(in srgb, $primary-color 20%, transparent);
+        transform: translateX(2px);
+
+        svg {
+          stroke-width: 2.5;
+        }
       }
     }
   }
@@ -226,6 +234,11 @@ $transition: all 0.3s ease;
         border-color: $danger-color;
         color: $danger-color;
       }
+
+      &:active {
+        transform: scale(0.98);
+        box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
+      }
     }
   }
 
@@ -247,6 +260,173 @@ $transition: all 0.3s ease;
   }
 }
 
+// 全局按钮点击反馈样式
+button,
+.btn-primary,
+.nav-item,
+.view-all-link,
+.logout-btn {
+  &:active {
+    transform: scale(0.98);
+    box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
+    transition: transform 0.1s ease;
+  }
+}
+
+.btn-primary {
+  &:active {
+    background-color: $primary-dark;
+    transform: scale(0.98);
+    box-shadow: 0 2px 4px color-mix(in srgb, $primary-color 40%, transparent);
+  }
+}
+
+.search-button,
+.menu-toggle,
+.notification-btn {
+  &:active {
+    transform: scale(0.95);
+  }
+}
+
+// 通知按钮样式增强
+.notification-btn {
+  &:active {
+    transform: scale(0.95);
+    box-shadow: 0 0 0 2px color-mix(in srgb, $primary-color 20%, transparent);
+  }
+  
+  // 确保通知按钮有足够的z-index,以免被下拉菜单遮挡
+  position: relative;
+  z-index: 1001;
+}
+
+// 通知下拉菜单
+.notification-dropdown {
+  position: absolute;
+  top: 100%;
+  right: 0;
+  margin-top: 8px;
+  width: 360px;
+  background-color: $background-primary;
+  border-radius: $border-radius;
+  box-shadow: $shadow-lg;
+  z-index: 1000;
+  border: 1px solid $border-color;
+  overflow: hidden;
+  animation: slideDown 0.3s ease-out;
+}
+
+@keyframes slideDown {
+  from {
+    opacity: 0;
+    transform: translateY(-10px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+.notification-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16px 20px;
+  border-bottom: 1px solid $border-color;
+  background-color: $background-tertiary;
+
+  h3 {
+    font-size: 16px;
+    font-weight: 600;
+    color: $text-primary;
+    margin: 0;
+  }
+
+  .close-btn {
+    background: none;
+    border: none;
+    cursor: pointer;
+    color: $text-secondary;
+    padding: 4px;
+    transition: $transition;
+
+    &:hover {
+      color: $text-primary;
+    }
+
+    &:active {
+      transform: scale(0.9);
+    }
+  }
+}
+
+.notification-list {
+  max-height: 400px;
+  overflow-y: auto;
+
+  .notification-item {
+    display: flex;
+    align-items: flex-start;
+    gap: 12px;
+    padding: 16px 20px;
+    border-bottom: 1px solid $border-color;
+    transition: $transition;
+
+    &:hover {
+      background-color: $background-tertiary;
+    }
+
+    &:last-child {
+      border-bottom: none;
+    }
+
+    .notification-icon {
+      flex-shrink: 0;
+      width: 32px;
+      height: 32px;
+      border-radius: 50%;
+      background-color: color-mix(in srgb, $primary-color 10%, transparent);
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      color: $primary-color;
+      margin-top: 2px;
+    }
+
+    .notification-content {
+      flex: 1;
+
+      p {
+        margin: 0;
+        font-size: 14px;
+        color: $text-secondary;
+        line-height: 1.4;
+      }
+    }
+  }
+}
+
+.notification-footer {
+  padding: 12px 20px;
+  text-align: center;
+  background-color: $background-tertiary;
+  border-top: 1px solid $border-color;
+
+  .view-all {
+    color: $primary-color;
+    text-decoration: none;
+    font-size: 14px;
+    font-weight: 500;
+    transition: $transition;
+
+    &:hover {
+      text-decoration: underline;
+      color: $primary-dark;
+    }
+  }
+}
+
 // 响应式设计
 @media (max-width: 1024px) {
   .sidebar {

+ 30 - 1
src/app/pages/customer-service/customer-service-layout/customer-service-layout.ts

@@ -1,6 +1,6 @@
 import { Component, signal } from '@angular/core';
 import { CommonModule } from '@angular/common';
-import { RouterOutlet, RouterLinkActive } from '@angular/router';
+import { Router, RouterOutlet, RouterLinkActive } from '@angular/router';
 import { FormsModule } from '@angular/forms';
 
 @Component({
@@ -14,8 +14,37 @@ export class CustomerServiceLayout {
   sidebarOpen = true;
   searchTerm = '';
   currentDate = new Date();
+  
+  // 通知相关状态
+  notifications = signal<string[]>([
+    '客户张三提交了新的咨询请求',
+    '项目A设计方案已完成审核',
+    '紧急任务需要处理:客户李四的修改需求'
+  ]);
+  showNotifications = signal(false);
+
+  constructor(private router: Router) {}
 
   toggleSidebar() {
     this.sidebarOpen = !this.sidebarOpen;
   }
+
+  // 切换消息通知显示状态
+  toggleNotifications() {
+    this.showNotifications.set(!this.showNotifications());
+  }
+
+  // 退出登录功能
+  logout() {
+    // 在实际应用中,这里会清除用户会话、token等
+    console.log('用户退出登录');
+    // 重定向到登录页面
+    this.router.navigate(['/login']);
+  }
+
+  // 处理全局搜索
+  handleSearch() {
+    console.log('执行全局搜索:', this.searchTerm);
+    // 在实际应用中,这里会执行搜索逻辑
+  }
 }

+ 84 - 59
src/app/pages/customer-service/dashboard/dashboard.html

@@ -7,70 +7,70 @@
 <!-- 数据看板 -->
 <section class="stats-dashboard">
   <div class="stats-grid">
-    <div class="stat-card">
-      <div class="stat-icon primary">
-        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
-          <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
-        </svg>
-      </div>
-      <div class="stat-content">
-        <div class="stat-value">{{ stats.newConsultations() }}</div>
-        <div class="stat-label">新咨询数</div>
-      </div>
-      <div class="stat-trend positive">
-        <span>+12%</span>
+      <div class="stat-card" (click)="handleNewConsultationsClick()" title="点击查看新咨询详情">
+        <div class="stat-icon primary">
+          <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
+            <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
+          </svg>
+        </div>
+        <div class="stat-content">
+          <div class="stat-value">{{ stats.newConsultations() }}</div>
+          <div class="stat-label">新咨询数</div>
+        </div>
+        <div class="stat-trend positive">
+          <span>+12%</span>
+        </div>
       </div>
-    </div>
 
-    <div class="stat-card">
-      <div class="stat-icon secondary">
-        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
-          <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
-          <polyline points="22 4 12 14.01 9 11.01"></polyline>
-        </svg>
-      </div>
-      <div class="stat-content">
-        <div class="stat-value">{{ stats.pendingAssignments() }}</div>
-        <div class="stat-label">待派单数</div>
-      </div>
-      <div class="stat-trend neutral">
-        <span>持平</span>
+      <div class="stat-card" (click)="handlePendingAssignmentsClick()" title="点击查看待派单详情">
+        <div class="stat-icon secondary">
+          <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
+            <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
+            <polyline points="22 4 12 14.01 9 11.01"></polyline>
+          </svg>
+        </div>
+        <div class="stat-content">
+          <div class="stat-value">{{ stats.pendingAssignments() }}</div>
+          <div class="stat-label">待派单数</div>
+        </div>
+        <div class="stat-trend neutral">
+          <span>持平</span>
+        </div>
       </div>
-    </div>
 
-    <div class="stat-card">
-      <div class="stat-icon warning">
-        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
-          <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
-          <line x1="12" y1="9" x2="12" y2="13"></line>
-          <line x1="12" y1="17" x2="12.01" y2="17"></line>
-        </svg>
-      </div>
-      <div class="stat-content">
-        <div class="stat-value">{{ stats.exceptionProjects() }}</div>
-        <div class="stat-label">异常项目</div>
-      </div>
-      <div class="stat-trend negative">
-        <span>+1</span>
+      <div class="stat-card" (click)="handleExceptionProjectsClick()" title="点击查看异常项目详情">
+        <div class="stat-icon warning">
+          <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
+            <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
+            <line x1="12" y1="9" x2="12" y2="13"></line>
+            <line x1="12" y1="17" x2="12.01" y2="17"></line>
+          </svg>
+        </div>
+        <div class="stat-content">
+          <div class="stat-value">{{ stats.exceptionProjects() }}</div>
+          <div class="stat-label">异常项目</div>
+        </div>
+        <div class="stat-trend negative">
+          <span>+1</span>
+        </div>
       </div>
-    </div>
 
-    <div class="stat-card">
-      <div class="stat-icon success">
-        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
-          <line x1="12" y1="1" x2="12" y2="23"></line>
-          <path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>
-        </svg>
-      </div>
-      <div class="stat-content">
-        <div class="stat-value">¥{{ stats.todayRevenue() }}</div>
-        <div class="stat-label">今日成交额</div>
-      </div>
-      <div class="stat-trend positive">
-        <span>+28%</span>
+      <div class="stat-card" (click)="handleTodayRevenueClick()" title="点击查看今日成交额详情">
+        <div class="stat-icon success">
+          <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
+            <line x1="12" y1="1" x2="12" y2="23"></line>
+            <path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>
+          </svg>
+        </div>
+        <div class="stat-content">
+          <div class="stat-value">¥{{ stats.todayRevenue() }}</div>
+          <div class="stat-label">今日成交额</div>
+        </div>
+        <div class="stat-trend positive">
+          <span>+28%</span>
+        </div>
       </div>
     </div>
-  </div>
 </section>
 
 <!-- 紧急待办和项目动态流 -->
@@ -79,7 +79,16 @@
   <section class="urgent-tasks-section">
     <div class="section-header">
       <h3>紧急待办</h3>
-      <a href="/customer-service/project-list" class="view-all-link">查看全部</a>
+      <div style="display: flex; gap: 12px; align-items: center;">
+        <button 
+          class="btn-primary"
+          (click)="addUrgentTask()"
+          style="font-size: 14px; padding: 6px 16px;"
+        >
+          添加紧急事项
+        </button>
+        <a href="/customer-service/project-list" class="view-all-link">查看全部</a>
+      </div>
     </div>
     
     <div class="tasks-list">
@@ -102,9 +111,25 @@
             <span class="task-time">{{ formatDate(task.deadline) }}</span>
             <span class="task-status">{{ getTaskStatus(task) }}</span>
           </div>
+          
+          <!-- 任务处理进度条 -->
+          <ng-container *ngIf="taskProcessingState()[task.id]?.inProgress">
+            <div class="task-progress-container">
+              <div class="task-progress-bar" [style.width]="taskProcessingState()[task.id]?.progress + '%'">
+                <span class="task-progress-text">{{ taskProcessingState()[task.id]?.progress }}%</span>
+              </div>
+            </div>
+          </ng-container>
         </div>
-        <div class="task-actions">
-          <button class="btn-primary" (click)="handleAssignment(task.projectId)">处理</button>
+            <div class="task-actions">
+          <button 
+            class="btn-primary"
+            [class.processing]="taskProcessingState()[task.id]?.inProgress"
+            (click)="handleAssignment(task.id)"
+            [disabled]="taskProcessingState()[task.id]?.inProgress || task.isCompleted"
+          >
+            {{ taskProcessingState()[task.id]?.inProgress ? '处理中' : '处理' }}
+          </button>
         </div>
       </div>
     </div>

+ 306 - 110
src/app/pages/customer-service/dashboard/dashboard.scss

@@ -18,56 +18,75 @@ $shadow-lg: 0 10px 30px rgba(0, 0, 0, 0.1);
 $border-radius: 8px;
 $transition: all 0.3s ease;
 
+// 增强对比度与可读性
+$text-primary-dark: #111827;
+$text-secondary-dark: #374151;
+$text-tertiary-dark: #6B7280;
+$shadow-hover: 0 8px 16px rgba(0, 0, 0, 0.08);
+
 // 欢迎区域
 .welcome-section {
-  margin-bottom: 24px;
+  margin-bottom: 32px;
+  padding: 24px;
+  background-color: $background-primary;
+  border-radius: $border-radius;
+  box-shadow: $shadow-sm;
+  border-left: 4px solid $primary-color;
 
   h2 {
-    font-size: 24px;
+    font-size: 28px;
     font-weight: 600;
-    margin-bottom: 8px;
-    color: $text-primary;
+    margin-bottom: 12px;
+    color: $text-primary-dark;
   }
 
   p {
-    font-size: 14px;
-    color: $text-secondary;
+    font-size: 16px;
+    color: $text-secondary-dark;
   }
 }
 
 // 数据看板
 .stats-dashboard {
-  margin-bottom: 24px;
+  margin-bottom: 32px;
 
   .stats-grid {
     display: grid;
-    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
-    gap: 16px;
+    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+    gap: 20px;
   }
 
   .stat-card {
     display: flex;
     align-items: center;
-    gap: 16px;
-    padding: 20px;
+    gap: 20px;
+    padding: 24px;
     background-color: $background-primary;
     border-radius: $border-radius;
-    box-shadow: $shadow-sm;
+    box-shadow: $shadow-md;
     transition: $transition;
+    cursor: pointer;
+    user-select: none;
 
     &:hover {
-      box-shadow: $shadow-md;
+      box-shadow: $shadow-hover;
       transform: translateY(-2px);
     }
 
+    &:active {
+      transform: translateY(0) scale(0.98);
+      box-shadow: $shadow-sm;
+    }
+
     .stat-icon {
-      width: 48px;
-      height: 48px;
-      border-radius: 12px;
+      width: 56px;
+      height: 56px;
+      border-radius: 16px;
       display: flex;
       align-items: center;
       justify-content: center;
       color: white;
+      transition: $transition;
 
       &.primary {
         background-color: $primary-color;
@@ -90,37 +109,40 @@ $transition: all 0.3s ease;
       flex: 1;
 
       .stat-value {
-        font-size: 28px;
-        font-weight: 600;
-        color: $text-primary;
-        margin-bottom: 4px;
+        font-size: 32px;
+        font-weight: 700;
+        color: $text-primary-dark;
+        margin-bottom: 6px;
+        line-height: 1.2;
       }
 
       .stat-label {
-        font-size: 14px;
-        color: $text-secondary;
+        font-size: 16px;
+        color: $text-secondary-dark;
+        font-weight: 500;
       }
     }
 
     .stat-trend {
-      padding: 4px 10px;
-      border-radius: 12px;
-      font-size: 12px;
-      font-weight: 500;
+      padding: 6px 12px;
+      border-radius: 16px;
+      font-size: 14px;
+      font-weight: 600;
+      transition: $transition;
 
       &.positive {
-        background-color: color-mix(in srgb, $success-color 10%, transparent);
+        background-color: color-mix(in srgb, $success-color 15%, transparent);
         color: $success-color;
       }
 
       &.negative {
-        background-color: color-mix(in srgb, $danger-color 10%, transparent);
+        background-color: color-mix(in srgb, $danger-color 15%, transparent);
         color: $danger-color;
       }
 
       &.neutral {
-        background-color: color-mix(in srgb, $text-tertiary 10%, transparent);
-        color: $text-tertiary;
+        background-color: color-mix(in srgb, $text-tertiary-dark 15%, transparent);
+        color: $text-tertiary-dark;
       }
     }
   }
@@ -130,8 +152,8 @@ $transition: all 0.3s ease;
 .content-grid {
   display: grid;
   grid-template-columns: 1fr 1fr;
-  gap: 24px;
-  margin-bottom: 24px;
+  gap: 28px;
+  margin-bottom: 32px;
 }
 
 // 通用区块头部
@@ -139,45 +161,61 @@ $transition: all 0.3s ease;
   display: flex;
   align-items: center;
   justify-content: space-between;
-  margin-bottom: 16px;
+  margin-bottom: 20px;
+  padding-bottom: 12px;
+  border-bottom: 2px solid $background-secondary;
 
   h3 {
-    font-size: 18px;
-    font-weight: 600;
-    color: $text-primary;
+    font-size: 20px;
+    font-weight: 700;
+    color: $text-primary-dark;
+    display: flex;
+    align-items: center;
+    gap: 8px;
   }
 
   .view-all-link {
     color: $primary-color;
     text-decoration: none;
-    font-size: 14px;
+    font-size: 16px;
+    font-weight: 500;
     transition: $transition;
+    display: flex;
+    align-items: center;
+    gap: 4px;
 
     &:hover {
       text-decoration: underline;
+      color: $primary-dark;
+    }
+
+    &::after {
+      content: '→';
+      font-size: 14px;
     }
   }
 
   .search-box {
-    max-width: 200px;
+    max-width: 220px;
 
     .search-input {
       width: 100%;
-      padding: 6px 12px;
+      padding: 8px 16px;
       border: 1px solid $border-color;
       border-radius: $border-radius;
-      font-size: 14px;
+      font-size: 16px;
       background-color: $background-primary;
-      color: $text-primary;
+      color: $text-primary-dark;
+      transition: $transition;
 
       &::placeholder {
-        color: $text-tertiary;
+        color: $text-tertiary-dark;
       }
 
       &:focus {
         outline: none;
         border-color: $primary-color;
-        box-shadow: 0 0 0 2px color-mix(in srgb, $primary-color 20%, transparent);
+        box-shadow: 0 0 0 3px color-mix(in srgb, $primary-color 20%, transparent);
       }
     }
   }
@@ -188,46 +226,56 @@ $transition: all 0.3s ease;
   background-color: $background-primary;
   border-radius: $border-radius;
   padding: 24px;
-  box-shadow: $shadow-sm;
+  box-shadow: $shadow-md;
+  transition: $transition;
+
+  &:hover {
+    box-shadow: $shadow-hover;
+  }
 
   .tasks-list {
     display: flex;
     flex-direction: column;
-    gap: 12px;
+    gap: 16px;
   }
 
   .empty-state {
     text-align: center;
     padding: 40px 20px;
-    color: $text-tertiary;
+    color: $text-tertiary-dark;
 
     svg {
       margin-bottom: 16px;
       opacity: 0.5;
+      width: 64px;
+      height: 64px;
     }
 
     p {
-      font-size: 14px;
+      font-size: 16px;
     }
   }
 
   .task-item {
     display: flex;
     align-items: flex-start;
-    gap: 12px;
-    padding: 16px;
+    gap: 16px;
+    padding: 20px;
     border: 1px solid $border-color;
     border-radius: $border-radius;
     transition: $transition;
+    position: relative;
 
     &:hover {
       border-color: $primary-color;
-      box-shadow: 0 2px 8px color-mix(in srgb, $primary-color 10%, transparent);
+      box-shadow: 0 4px 12px color-mix(in srgb, $primary-color 10%, transparent);
+      transform: translateY(-1px);
     }
 
     &.completed {
-      opacity: 0.6;
+      opacity: 0.7;
       background-color: $background-tertiary;
+      border-color: $success-color;
     }
 
     &.overdue {
@@ -239,8 +287,8 @@ $transition: all 0.3s ease;
       margin-top: 2px;
 
       input[type="checkbox"] {
-        width: 18px;
-        height: 18px;
+        width: 20px;
+        height: 20px;
         cursor: pointer;
       }
     }
@@ -249,29 +297,110 @@ $transition: all 0.3s ease;
       flex: 1;
 
       .task-title {
-        font-size: 16px;
-        font-weight: 500;
-        color: $text-primary;
-        margin-bottom: 4px;
+        font-size: 18px;
+        font-weight: 600;
+        color: $text-primary-dark;
+        margin-bottom: 6px;
+        line-height: 1.3;
       }
 
       .task-project {
-        font-size: 13px;
-        color: $text-secondary;
-        margin-bottom: 8px;
+        font-size: 16px;
+        color: $text-secondary-dark;
+        margin-bottom: 10px;
       }
 
       .task-meta {
         display: flex;
-        gap: 16px;
+        gap: 20px;
+        font-size: 14px;
+        color: $text-tertiary-dark;
+      }
+
+      // 任务处理进度条
+      .task-progress-container {
+        margin-top: 12px;
+        height: 8px;
+        background-color: $background-secondary;
+        border-radius: 4px;
+        overflow: hidden;
+        position: relative;
+      }
+
+      .task-progress-bar {
+        height: 100%;
+        background-color: $primary-color;
+        border-radius: 4px;
+        transition: width 0.3s ease;
+        position: relative;
+        overflow: hidden;
+
+        &::after {
+          content: '';
+          position: absolute;
+          top: 0;
+          left: 0;
+          right: 0;
+          bottom: 0;
+          background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
+          animation: progressAnimation 1.5s infinite;
+        }
+
+        @keyframes progressAnimation {
+          0% {
+            transform: translateX(-100%);
+          }
+          100% {
+            transform: translateX(100%);
+          }
+        }
+      }
+
+      .task-progress-text {
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
         font-size: 12px;
-        color: $text-tertiary;
+        font-weight: 600;
+        color: white;
+        text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
       }
     }
 
     .task-actions {
       display: flex;
       align-items: flex-start;
+      gap: 8px;
+    }
+  }
+
+  // 按钮样式
+  .btn-primary {
+    padding: 10px 20px;
+    background-color: $primary-color;
+    color: white;
+    border: none;
+    border-radius: $border-radius;
+    font-size: 16px;
+    font-weight: 600;
+    cursor: pointer;
+    transition: $transition;
+
+    &:hover {
+      background-color: $primary-dark;
+      transform: translateY(-1px);
+      box-shadow: 0 4px 8px color-mix(in srgb, $primary-color 30%, transparent);
+    }
+
+    &:active {
+      transform: translateY(0);
+    }
+
+    &.processing {
+      background-color: $secondary-color;
+      position: relative;
+      cursor: not-allowed;
     }
   }
 }
@@ -281,7 +410,12 @@ $transition: all 0.3s ease;
   background-color: $background-primary;
   border-radius: $border-radius;
   padding: 24px;
-  box-shadow: $shadow-sm;
+  box-shadow: $shadow-md;
+  transition: $transition;
+
+  &:hover {
+    box-shadow: $shadow-hover;
+  }
 
   .updates-list {
     display: flex;
@@ -292,89 +426,105 @@ $transition: all 0.3s ease;
   .empty-state {
     text-align: center;
     padding: 40px 20px;
-    color: $text-tertiary;
+    color: $text-tertiary-dark;
 
     svg {
       margin-bottom: 16px;
       opacity: 0.5;
+      width: 64px;
+      height: 64px;
     }
 
     p {
-      font-size: 14px;
+      font-size: 16px;
     }
   }
 
   .update-item {
     display: flex;
-    gap: 12px;
-    padding: 16px;
+    gap: 16px;
+    padding: 20px;
     border: 1px solid $border-color;
     border-radius: $border-radius;
     transition: $transition;
 
     &:hover {
       border-color: $primary-color;
-      box-shadow: 0 2px 8px color-mix(in srgb, $primary-color 10%, transparent);
+      box-shadow: 0 4px 12px color-mix(in srgb, $primary-color 10%, transparent);
+      transform: translateY(-2px);
     }
 
     .update-icon {
-      width: 40px;
-      height: 40px;
-      border-radius: 10px;
-      background-color: color-mix(in srgb, $primary-color 10%, transparent);
+      width: 44px;
+      height: 44px;
+      background-color: $primary-color;
+      border-radius: 12px;
       display: flex;
       align-items: center;
       justify-content: center;
-      color: $primary-color;
+      color: white;
       flex-shrink: 0;
+      transition: $transition;
     }
 
     .update-content {
       flex: 1;
 
       .update-title {
-        font-size: 15px;
-        font-weight: 500;
-        color: $text-primary;
-        margin-bottom: 4px;
+        font-size: 18px;
+        font-weight: 600;
+        color: $text-primary-dark;
+        margin-bottom: 6px;
+        line-height: 1.3;
       }
 
       .update-text {
-        font-size: 14px;
-        color: $text-secondary;
-        margin-bottom: 8px;
-        line-height: 1.5;
+        font-size: 16px;
+        color: $text-secondary-dark;
+        margin-bottom: 12px;
+        line-height: 1.4;
       }
 
       .update-meta {
         display: flex;
-        justify-content: space-between;
         align-items: center;
+        gap: 20px;
 
         .update-time {
-          font-size: 12px;
-          color: $text-tertiary;
+          font-size: 14px;
+          color: $text-tertiary-dark;
         }
 
         .update-status {
-          font-size: 12px;
-          font-weight: 500;
-          padding: 2px 8px;
-          border-radius: 10px;
+          padding: 4px 12px;
+          border-radius: 16px;
+          font-size: 14px;
+          font-weight: 600;
+          transition: $transition;
+
+          &.status-active {
+            background-color: color-mix(in srgb, $primary-color 15%, transparent);
+            color: $primary-color;
+          }
 
-          &.completed {
-            background-color: color-mix(in srgb, $success-color 10%, transparent);
+          &.status-completed {
+            background-color: color-mix(in srgb, $success-color 15%, transparent);
             color: $success-color;
           }
 
-          &.pending {
-            background-color: color-mix(in srgb, $primary-color 10%, transparent);
-            color: $primary-color;
+          &.status-warning {
+            background-color: color-mix(in srgb, $warning-color 15%, transparent);
+            color: $warning-color;
+          }
+
+          &.status-success {
+            background-color: color-mix(in srgb, $success-color 15%, transparent);
+            color: $success-color;
           }
 
-          &.exception {
-            background-color: color-mix(in srgb, $danger-color 10%, transparent);
-            color: $danger-color;
+          &.status-info {
+            background-color: color-mix(in srgb, $primary-color 15%, transparent);
+            color: $primary-color;
           }
         }
       }
@@ -415,31 +565,77 @@ $transition: all 0.3s ease;
 // 回到顶部按钮
 .back-to-top {
   position: fixed;
-  bottom: 20px;
-  right: 20px;
-  width: 40px;
-  height: 40px;
+  bottom: 24px;
+  right: 24px;
+  width: 48px;
+  height: 48px;
   border-radius: 50%;
-  background-color: #1976d2;
+  background-color: $primary-color;
   color: white;
+  border: none;
   display: flex;
   align-items: center;
   justify-content: center;
   cursor: pointer;
+  box-shadow: $shadow-md;
+  transition: $transition;
   opacity: 0;
   visibility: hidden;
-  transition: opacity 0.3s, visibility 0.3s;
-  border: none;
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
-}
+  z-index: 1000;
 
-.back-to-top.visible {
-  opacity: 1;
-  visibility: visible;
+  &.visible {
+    opacity: 1;
+    visibility: visible;
+  }
+
+  &:hover {
+    background-color: $primary-dark;
+    transform: translateY(-2px);
+    box-shadow: $shadow-lg;
+  }
+
+  &:active {
+    transform: translateY(0);
+  }
 }
 
-.back-to-top:hover {
-  background-color: #1565c0;
+// 响应式设计
+@media (max-width: 768px) {
+  .content-grid {
+    grid-template-columns: 1fr;
+    gap: 20px;
+  }
+
+  .stats-grid {
+    grid-template-columns: 1fr;
+    gap: 16px;
+  }
+
+  .welcome-section {
+    padding: 20px;
+  }
+
+  .welcome-section h2 {
+    font-size: 24px;
+  }
+
+  .welcome-section p {
+    font-size: 14px;
+  }
+
+  .section-header h3 {
+    font-size: 18px;
+  }
+
+  .task-title,
+  .update-title {
+    font-size: 16px !important;
+  }
+
+  .task-project,
+  .update-text {
+    font-size: 14px !important;
+  }
 }
 
 // 响应式设计

+ 89 - 3
src/app/pages/customer-service/dashboard/dashboard.ts

@@ -24,6 +24,9 @@ export class Dashboard implements OnInit, OnDestroy {
 
   // 紧急待办列表
   urgentTasks = signal<Task[]>([]);
+
+  // 任务处理状态
+  taskProcessingState = signal<{[key: string]: {inProgress: boolean, progress: number}}>({});
   
   // 项目动态流
   projectUpdates = signal<(Project | CustomerFeedback)[]>([]);
@@ -130,13 +133,96 @@ export class Dashboard implements OnInit, OnDestroy {
   }
 
   // 处理派单操作
-  handleAssignment(projectId: string): void {
-    // 在实际应用中,这里会调用API处理派单
-    console.log('处理派单:', projectId);
+  handleAssignment(taskId: string): void {
+    // 标记任务为处理中
+    const task = this.urgentTasks().find(t => t.id === taskId);
+    if (task) {
+      // 初始化处理状态
+      this.taskProcessingState.update(state => ({
+        ...state,
+        [task.id]: { inProgress: true, progress: 0 }
+      }));
+
+      // 模拟处理进度
+      let progress = 0;
+      const interval = setInterval(() => {
+        progress += 10;
+        
+        this.taskProcessingState.update(state => ({
+          ...state,
+          [task.id]: { inProgress: progress < 100, progress }
+        }));
+
+        if (progress >= 100) {
+          clearInterval(interval);
+          
+          // 处理完成后从列表中移除该任务
+          this.urgentTasks.set(
+            this.urgentTasks().filter(t => t.id !== task.id)
+          );
+          
+          // 清除处理状态
+          this.taskProcessingState.update(state => {
+            const newState = { ...state };
+            delete newState[task.id];
+            return newState;
+          });
+        }
+      }, 300);
+    }
+
     // 更新统计数据
     this.stats.pendingAssignments.set(this.stats.pendingAssignments() - 1);
   }
 
+  // 添加新的紧急事项
+  addUrgentTask(): void {
+    // 在实际应用中,这里可能会打开一个表单模态框
+    // 这里使用模拟数据直接添加
+    const newTask: Task = {
+      id: `task-${Date.now()}`,
+      projectId: `project-${Math.floor(Math.random() * 1000)}`,
+      title: '新增紧急任务',
+      projectName: '新项目',
+      stage: '前期沟通', // 设置一个有效的ProjectStage值
+      deadline: new Date(),
+      isOverdue: false,
+      isCompleted: false,
+      // Task接口中没有status属性,移除它
+      // 为了符合Task接口要求,添加required的stage字段
+      priority: 'high', // 模拟值
+      assignee: '当前用户', // 模拟值
+      description: '紧急任务描述' // 模拟值
+    };
+
+    this.urgentTasks.set([newTask, ...this.urgentTasks()]);
+    this.stats.pendingAssignments.set(this.stats.pendingAssignments() + 1);
+  }
+
+  // 新咨询数图标点击处理
+  handleNewConsultationsClick(): void {
+    console.log('点击查看新咨询详情');
+    // 在实际应用中,这里会跳转到新咨询列表页面或打开新咨询模态框
+  }
+
+  // 待派单数图标点击处理
+  handlePendingAssignmentsClick(): void {
+    console.log('点击查看待派单详情');
+    // 在实际应用中,这里会跳转到待派单列表页面或打开待派单模态框
+  }
+
+  // 异常项目图标点击处理
+  handleExceptionProjectsClick(): void {
+    console.log('点击查看异常项目详情');
+    // 在实际应用中,这里会跳转到异常项目列表页面或打开异常项目模态框
+  }
+
+  // 今日成交额图标点击处理
+  handleTodayRevenueClick(): void {
+    console.log('点击查看今日成交额详情');
+    // 在实际应用中,这里会跳转到今日成交额详情页面或打开今日成交额模态框
+  }
+
   // 格式化日期
   formatDate(date: Date | string): string {
     if (!date) return '';

+ 408 - 0
src/app/pages/hr/assets/assets-stats.html

@@ -0,0 +1,408 @@
+<div class="assets-stats-container">
+  <header class="page-header">
+    <h1>资产管理与统计</h1>
+    <p class="page-description">管理企业全类型资产,查看资产使用状态和统计数据</p>
+  </header>
+
+  <!-- 资产分类筛选栏 -->
+  <div class="filter-bar">
+    <div class="search-container">
+      <mat-icon class="search-icon">search</mat-icon>
+      <input 
+        matInput 
+        placeholder="搜索资产名称、序列号、使用人..."
+        [value]="searchTerm()"
+        (input)="searchTerm.set($event.target.value)"
+        class="search-input"
+      >
+      <button mat-icon-button *ngIf="searchTerm()" (click)="searchTerm.set('')" class="clear-search">
+        <mat-icon>close</mat-icon>
+      </button>
+    </div>
+    
+    <div class="filter-controls">
+      <div class="filter-group">
+        <label>资产类型:</label>
+        <select [value]="typeFilter()" (change)="typeFilter.set($any($event.target).value)" class="filter-select">
+          <option value="">全部类型</option>
+          <option *ngFor="let type of assetTypes()" [value]="type">{{ type }}</option>
+        </select>
+      </div>
+      
+      <div class="filter-group">
+        <label>资产状态:</label>
+        <select [value]="statusFilter()" (change)="statusFilter.set($any($event.target).value)" class="filter-select">
+          <option value="">全部状态</option>
+          <option value="空闲">空闲</option>
+          <option value="占用">占用</option>
+          <option value="故障">故障</option>
+          <option value="报修中">报修中</option>
+        </select>
+      </div>
+      
+      <div class="filter-group">
+        <label>所属部门:</label>
+        <select [value]="departmentFilter()" (change)="departmentFilter.set($any($event.target).value)" class="filter-select">
+          <option value="">全部部门</option>
+          <option *ngFor="let dept of departments()" [value]="dept">{{ dept }}</option>
+        </select>
+      </div>
+      
+      <button mat-button color="primary" (click)="resetFilters()" class="reset-btn">
+        <mat-icon>refresh</mat-icon>
+        重置筛选
+      </button>
+      
+      <button mat-raised-button color="primary" (click)="exportAssetLedger()" class="export-btn">
+        <mat-icon>file_download</mat-icon>
+        导出台账
+      </button>
+    </div>
+  </div>
+
+  <!-- 视图切换和统计卡片 -->
+  <div class="view-stats-section">
+    <div class="view-toggle">
+      <button 
+        mat-button 
+        [class.active]="selectedView() === 'grid'"
+        (click)="switchView('grid')"
+        class="view-btn"
+      >
+        <mat-icon>grid_view</mat-icon>
+        卡片视图
+      </button>
+      <button 
+        mat-button 
+        [class.active]="selectedView() === 'list'"
+        (click)="switchView('list')"
+        class="view-btn"
+      >
+        <mat-icon>list</mat-icon>
+        列表视图
+      </button>
+      <div class="asset-count">
+        共 {{ filteredAssets().length }} 项资产
+      </div>
+    </div>
+    
+    <div class="stats-cards">
+      <div class="stat-card">
+        <div class="stat-icon">
+          <mat-icon>category</mat-icon>
+        </div>
+        <div class="stat-content">
+          <div class="stat-value">{{ assetStats().total }}</div>
+          <div class="stat-label">资产总数</div>
+        </div>
+      </div>
+      <div class="stat-card occupied">
+        <div class="stat-icon">
+          <mat-icon>person</mat-icon>
+        </div>
+        <div class="stat-content">
+          <div class="stat-value">{{ assetStats().occupied }}</div>
+          <div class="stat-label">占用中</div>
+          <div class="stat-percentage">{{ Math.round(assetStats().occupied / assetStats().total * 100) }}%</div>
+        </div>
+      </div>
+      <div class="stat-card idle">
+        <div class="stat-icon">
+          <mat-icon>schedule</mat-icon>
+        </div>
+        <div class="stat-content">
+          <div class="stat-value">{{ assetStats().idle }}</div>
+          <div class="stat-label">空闲</div>
+          <div class="stat-percentage">{{ Math.round(assetStats().idle / assetStats().total * 100) }}%</div>
+        </div>
+      </div>
+      <div class="stat-card faulty">
+        <div class="stat-icon">
+          <mat-icon>error</mat-icon>
+        </div>
+        <div class="stat-content">
+          <div class="stat-value">{{ assetStats().faulty }}</div>
+          <div class="stat-label">故障</div>
+          <div class="stat-percentage">{{ Math.round(assetStats().faulty / assetStats().total * 100) }}%</div>
+        </div>
+      </div>
+      <div class="stat-card repairing">
+        <div class="stat-icon">
+          <mat-icon>build</mat-icon>
+        </div>
+        <div class="stat-content">
+          <div class="stat-value">{{ assetStats().repairing }}</div>
+          <div class="stat-label">报修中</div>
+          <div class="stat-percentage">{{ Math.round(assetStats().repairing / assetStats().total * 100) }}%</div>
+        </div>
+      </div>
+      <div class="stat-card value">
+        <div class="stat-icon">
+          <mat-icon>monetization_on</mat-icon>
+        </div>
+        <div class="stat-content">
+          <div class="stat-value">{{ formatCurrency(assetStats().totalValue) }}</div>
+          <div class="stat-label">资产总值</div>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <!-- 资产列表/网格视图 -->
+  <div class="assets-view">
+    <!-- 网格视图 -->
+    <div *ngIf="selectedView() === 'grid'" class="assets-grid">
+      <div 
+        *ngFor="let asset of filteredAssets()"
+        class="asset-card"
+        [class.faulty]="asset.status === '故障' || asset.status === '报修中'"
+      >
+        <div class="asset-header">
+          <div class="asset-type-icon" [class]="asset.type">
+            <mat-icon>{{ getTypeIcon(asset.type) }}</mat-icon>
+          </div>
+          <div class="asset-status" [class]="getStatusClass(asset.status)">
+            {{ asset.status }}
+          </div>
+        </div>
+        <div class="asset-content">
+          <h3 class="asset-name">{{ asset.name }}</h3>
+          <div class="asset-info">
+            <div class="info-item">
+              <span class="info-label">序列号:</span>
+              <span class="info-value">{{ asset.serialNumber }}</span>
+            </div>
+            <div class="info-item">
+              <span class="info-label">购买日期:</span>
+              <span class="info-value">{{ formatDate(asset.purchaseDate) }}</span>
+            </div>
+            <div class="info-item">
+              <span class="info-label">价值:</span>
+              <span class="info-value">{{ formatCurrency(asset.value) }}</span>
+            </div>
+            <div class="info-item">
+              <span class="info-label">部门:</span>
+              <span class="info-value">{{ asset.department }}</span>
+            </div>
+            <div *ngIf="asset.assignedToName" class="info-item">
+              <span class="info-label">使用人:</span>
+              <span class="info-value">{{ asset.assignedToName }}</span>
+            </div>
+            <div *ngIf="asset.status === '故障' || asset.status === '报修中'" class="info-item faulty-notice">
+              <span class="info-label">故障描述:</span>
+              <span class="info-value">需要维修</span>
+            </div>
+          </div>
+        </div>
+        <div class="asset-actions">
+          <button mat-icon-button matTooltip="查看详情" class="action-btn">
+            <mat-icon>visibility</mat-icon>
+          </button>
+          <button 
+            mat-icon-button 
+            matTooltip="编辑信息"
+            class="action-btn"
+          >
+            <mat-icon>edit</mat-icon>
+          </button>
+          <ng-container *ngIf="asset.status === '空闲'">
+            <button 
+              mat-icon-button 
+              matTooltip="分配资产"
+              class="action-btn primary"
+              (click)="openAssignmentDialog(asset)"
+            >
+              <mat-icon>assignment_ind</mat-icon>
+            </button>
+          </ng-container>
+          <ng-container *ngIf="asset.status === '占用'">
+            <button 
+              mat-icon-button 
+              matTooltip="归还资产"
+              class="action-btn primary"
+              (click)="openAssignmentDialog(asset, true)"
+            >
+              <mat-icon>undo</mat-icon>
+            </button>
+          </ng-container>
+          <ng-container *ngIf="asset.status === '故障'">
+            <button 
+              mat-icon-button 
+              matTooltip="申请报修"
+              class="action-btn warning"
+              (click)="openRepairDialog(asset)"
+            >
+              <mat-icon>build</mat-icon>
+            </button>
+          </ng-container>
+        </div>
+      </div>
+    </div>
+    
+    <!-- 列表视图 -->
+    <div *ngIf="selectedView() === 'list'" class="assets-list">
+      <table mat-table [dataSource]="filteredAssets()" class="assets-table">
+        <ng-container matColumnDef="name">
+          <th mat-header-cell *matHeaderCellDef>资产名称</th>
+          <td mat-cell *matCellDef="let asset">
+            <div class="asset-name-list">
+              <div class="asset-type-icon-small" [class]="asset.type">
+                <mat-icon>{{ getTypeIcon(asset.type) }}</mat-icon>
+              </div>
+              <span>{{ asset.name }}</span>
+            </div>
+          </td>
+        </ng-container>
+        <ng-container matColumnDef="type">
+          <th mat-header-cell *matHeaderCellDef>类型</th>
+          <td mat-cell *matCellDef="let asset">{{ asset.type }}</td>
+        </ng-container>
+        <ng-container matColumnDef="status">
+          <th mat-header-cell *matHeaderCellDef>状态</th>
+          <td mat-cell *matCellDef="let asset">
+            <span class="status-badge" [class]="getStatusClass(asset.status)">
+              {{ asset.status }}
+            </span>
+          </td>
+        </ng-container>
+        <ng-container matColumnDef="department">
+          <th mat-header-cell *matHeaderCellDef>部门</th>
+          <td mat-cell *matCellDef="let asset">{{ asset.department }}</td>
+        </ng-container>
+        <ng-container matColumnDef="assignedTo">
+          <th mat-header-cell *matHeaderCellDef>使用人</th>
+          <td mat-cell *matCellDef="let asset">{{ asset.assignedToName || '-' }}</td>
+        </ng-container>
+        <ng-container matColumnDef="purchaseDate">
+          <th mat-header-cell *matHeaderCellDef>购买日期</th>
+          <td mat-cell *matCellDef="let asset">{{ formatDate(asset.purchaseDate) }}</td>
+        </ng-container>
+        <ng-container matColumnDef="value">
+          <th mat-header-cell *matHeaderCellDef>价值</th>
+          <td mat-cell *matCellDef="let asset">{{ formatCurrency(asset.value) }}</td>
+        </ng-container>
+        <ng-container matColumnDef="actions">
+          <th mat-header-cell *matHeaderCellDef>操作</th>
+          <td mat-cell *matCellDef="let asset" class="actions-column">
+            <div class="action-buttons-list">
+              <button mat-icon-button matTooltip="查看详情" class="action-btn">
+                <mat-icon>visibility</mat-icon>
+              </button>
+              <button mat-icon-button matTooltip="编辑信息" class="action-btn">
+                <mat-icon>edit</mat-icon>
+              </button>
+              <ng-container *ngIf="asset.status === '空闲'">
+                <button 
+                  mat-icon-button 
+                  matTooltip="分配资产"
+                  class="action-btn primary"
+                  (click)="openAssignmentDialog(asset)"
+                >
+                  <mat-icon>assignment_ind</mat-icon>
+                </button>
+              </ng-container>
+              <ng-container *ngIf="asset.status === '占用'">
+                <button 
+                  mat-icon-button 
+                  matTooltip="归还资产"
+                  class="action-btn primary"
+                  (click)="openAssignmentDialog(asset, true)"
+                >
+                  <mat-icon>undo</mat-icon>
+                </button>
+              </ng-container>
+              <ng-container *ngIf="asset.status === '故障'">
+                <button 
+                  mat-icon-button 
+                  matTooltip="申请报修"
+                  class="action-btn warning"
+                  (click)="openRepairDialog(asset)"
+                >
+                  <mat-icon>build</mat-icon>
+                </button>
+              </ng-container>
+            </div>
+          </td>
+        </ng-container>
+        
+        <tr mat-header-row *matHeaderRowDef="['name', 'type', 'status', 'department', 'assignedTo', 'purchaseDate', 'value', 'actions']"></tr>
+        <tr mat-row *matRowDef="let row; columns: ['name', 'type', 'status', 'department', 'assignedTo', 'purchaseDate', 'value', 'actions']"></tr>
+        
+        <tr class="mat-row" *matNoDataRow>
+          <td class="mat-cell" colspan="8" class="no-data">
+            <div class="empty-state">
+              <mat-icon>search_off</mat-icon>
+              <p>没有找到符合条件的资产</p>
+            </div>
+          </td>
+        </tr>
+      </table>
+    </div>
+  </div>
+
+  <!-- 统计图表区 -->
+  <div class="stats-section">
+    <!-- 按类型统计 -->
+    <div class="stats-card">
+      <div class="card-header">
+        <h2>资产类型统计</h2>
+      </div>
+      <div class="chart-content">
+        <div class="type-stats">
+          <div *ngFor="let typeStat of assetStats().typeStatsArray" class="type-stat-item">
+            <div class="type-info">
+              <span class="type-name">{{ typeStat.type }}</span>
+              <span class="type-count">{{ typeStat.count }}台</span>
+            </div>
+            <div class="progress-bar">
+              <div class="progress-fill" [style.width]="(typeStat.count / assetStats().total * 100) + '%'"></div>
+            </div>
+            <div class="type-value">{{ formatCurrency(typeStat.value) }}</div>
+          </div>
+        </div>
+      </div>
+    </div>
+    
+    <!-- 按部门统计 -->
+    <div class="stats-card">
+      <div class="card-header">
+        <h2>部门资产统计</h2>
+      </div>
+      <div class="chart-content">
+        <div class="department-stats">
+          <div *ngFor="let deptStat of assetStats().departmentStatsArray" class="department-stat-item">
+            <div class="department-info">
+              <span class="department-name">{{ deptStat.department }}</span>
+              <span class="department-count">{{ deptStat.count }}台</span>
+            </div>
+            <div class="progress-bar">
+              <div class="progress-fill" [style.width]="(deptStat.count / assetStats().total * 100) + '%'"></div>
+            </div>
+            <div class="department-value">{{ formatCurrency(deptStat.value) }}</div>
+          </div>
+        </div>
+      </div>
+    </div>
+    
+    <!-- 资产使用时长统计 -->
+    <div class="stats-card">
+      <div class="card-header">
+        <h2>资产平均使用时长(小时/月)</h2>
+      </div>
+      <div class="chart-content">
+        <div class="usage-stats">
+          <div *ngFor="let usage of assetStats().usageStats" class="usage-stat-item">
+            <div class="usage-info">
+              <span class="usage-type">{{ usage.type }}</span>
+            </div>
+            <div class="bar-container">
+              <div class="usage-bar" [style.height]="(usage.avgHours / 200 * 100) + '%'">
+                <span class="usage-value">{{ usage.avgHours }}h</span>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>

+ 1119 - 0
src/app/pages/hr/assets/assets-stats.scss

@@ -0,0 +1,1119 @@
+// 自定义主题
+$primary-color: #1e40af; // 深蓝主色,传递可靠感
+$primary-light: #3b82f6; // 浅蓝色,用于悬停效果
+$secondary-color: #0d9488; // 薄荷绿,作为强调色
+$success-color: #10b981; // 成功色
+$warning-color: #f59e0b; // 警告色
+$error-color: #ef4444; // 错误色
+$info-color: #3b82f6; // 信息色
+$text-primary: #1f2937; // 主要文本色
+$text-secondary: #4b5563; // 次要文本色
+$text-tertiary: #9ca3af; // 辅助文本色
+$bg-primary: #ffffff; // 主背景色
+$bg-secondary: #f9fafb; // 次要背景色
+$bg-tertiary: #f3f4f6; // 辅助背景色
+$border-color: #e5e7eb; // 边框色
+$shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+$shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+$shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+$border-radius: 8px;
+$transition: all 0.2s ease;
+
+// 主容器样式
+.assets-stats-container {
+  padding: 24px;
+  min-height: 100vh;
+  background-color: $bg-secondary;
+  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+}
+
+// 页面标题
+.page-header {
+  margin-bottom: 32px;
+  text-align: center;
+
+  h1 {
+    font-size: 32px;
+    font-weight: 700;
+    color: $text-primary;
+    margin: 0 0 8px 0;
+    letter-spacing: -0.5px;
+  }
+
+  .page-description {
+    font-size: 16px;
+    color: $text-secondary;
+    margin: 0;
+  }
+}
+
+// 资产分类筛选栏
+.filter-bar {
+  background-color: $bg-primary;
+  border-radius: $border-radius;
+  box-shadow: $shadow-sm;
+  padding: 20px;
+  margin-bottom: 24px;
+  transition: $transition;
+
+  &:hover {
+    box-shadow: $shadow-md;
+  }
+
+  .search-container {
+    position: relative;
+    max-width: 400px;
+    margin: 0 auto 20px;
+
+    .search-icon {
+      position: absolute;
+      left: 12px;
+      top: 50%;
+      transform: translateY(-50%);
+      color: $text-tertiary;
+      z-index: 1;
+    }
+
+    .search-input {
+      width: 100%;
+      padding: 12px 12px 12px 40px;
+      border: 1px solid $border-color;
+      border-radius: $border-radius;
+      font-size: 14px;
+      color: $text-primary;
+      background-color: $bg-primary;
+      transition: $transition;
+      box-sizing: border-box;
+
+      &:focus {
+        outline: none;
+        border-color: $primary-color;
+        box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.1);
+        transform: scale(1.01);
+      }
+
+      &::placeholder {
+        color: $text-tertiary;
+      }
+    }
+
+    .clear-search {
+      position: absolute;
+      right: 8px;
+      top: 50%;
+      transform: translateY(-50%);
+      color: $text-tertiary;
+      padding: 4px;
+      transition: $transition;
+
+      &:hover {
+        color: $text-primary;
+        background-color: $bg-tertiary;
+        border-radius: 50%;
+      }
+    }
+  }
+
+  .filter-controls {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 16px;
+    justify-content: center;
+    align-items: end;
+
+    .filter-group {
+      display: flex;
+      flex-direction: column;
+      gap: 6px;
+      min-width: 150px;
+
+      label {
+        font-size: 12px;
+        font-weight: 500;
+        color: $text-secondary;
+        text-transform: uppercase;
+        letter-spacing: 0.5px;
+      }
+
+      .filter-select {
+        padding: 10px 12px;
+        border: 1px solid $border-color;
+        border-radius: $border-radius;
+        font-size: 14px;
+        color: $text-primary;
+        background-color: $bg-primary;
+        transition: $transition;
+        cursor: pointer;
+        min-width: 150px;
+
+        &:focus {
+          outline: none;
+          border-color: $primary-color;
+          box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.1);
+        }
+
+        &:hover {
+          border-color: $primary-light;
+        }
+      }
+    }
+
+    .reset-btn {
+      padding: 8px 16px;
+      border-radius: $border-radius;
+      font-size: 14px;
+      font-weight: 500;
+      color: $primary-color;
+      transition: $transition;
+
+      &:hover {
+        background-color: color-mix(in srgb, $primary-color 5%, transparent);
+      }
+    }
+
+    .export-btn {
+      padding: 8px 16px;
+      border-radius: $border-radius;
+      font-size: 14px;
+      font-weight: 500;
+      background-color: $primary-color;
+      color: white;
+      transition: $transition;
+
+      &:hover {
+        background-color: $primary-light;
+        transform: translateY(-1px);
+        box-shadow: $shadow-md;
+      }
+
+      &:active {
+        transform: scale(0.98);
+        box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.1);
+      }
+    }
+  }
+}
+
+// 视图切换和统计卡片
+.view-stats-section {
+  margin-bottom: 24px;
+}
+
+.view-toggle {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  margin-bottom: 24px;
+  padding: 12px 20px;
+  background-color: $bg-primary;
+  border-radius: $border-radius;
+  box-shadow: $shadow-sm;
+
+  .view-btn {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    padding: 8px 16px;
+    border-radius: $border-radius;
+    font-size: 14px;
+    font-weight: 500;
+    color: $text-secondary;
+    transition: $transition;
+
+    &.active {
+      background-color: $primary-color;
+      color: white;
+      box-shadow: $shadow-sm;
+    }
+
+    &:not(.active):hover {
+      background-color: $bg-tertiary;
+      color: $text-primary;
+    }
+  }
+
+  .asset-count {
+    margin-left: auto;
+    font-size: 14px;
+    color: $text-secondary;
+    font-weight: 500;
+  }
+}
+
+.stats-cards {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+  gap: 16px;
+
+  .stat-card {
+    background-color: $bg-primary;
+    border-radius: $border-radius;
+    padding: 20px;
+    box-shadow: $shadow-sm;
+    display: flex;
+    align-items: center;
+    gap: 16px;
+    transition: $transition;
+    position: relative;
+    overflow: hidden;
+
+    &::before {
+      content: '';
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 4px;
+      height: 100%;
+      background-color: $primary-color;
+    }
+
+    &:hover {
+      transform: translateY(-4px);
+      box-shadow: $shadow-md;
+    }
+
+    .stat-icon {
+      width: 40px;
+      height: 40px;
+      border-radius: 50%;
+      background-color: color-mix(in srgb, $primary-color 10%, transparent);
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      color: $primary-color;
+      flex-shrink: 0;
+    }
+
+    .stat-content {
+      flex: 1;
+
+      .stat-value {
+        font-size: 24px;
+        font-weight: 700;
+        color: $text-primary;
+        margin-bottom: 4px;
+        line-height: 1;
+      }
+
+      .stat-label {
+        font-size: 12px;
+        color: $text-secondary;
+        font-weight: 500;
+        text-transform: uppercase;
+        letter-spacing: 0.5px;
+      }
+
+      .stat-percentage {
+        font-size: 12px;
+        color: $text-tertiary;
+        margin-top: 4px;
+      }
+    }
+
+    &.occupied {
+      &::before {
+        background-color: $primary-color;
+      }
+
+      .stat-icon {
+        background-color: color-mix(in srgb, $primary-color 10%, transparent);
+        color: $primary-color;
+      }
+    }
+
+    &.idle {
+      &::before {
+        background-color: $secondary-color;
+      }
+
+      .stat-icon {
+        background-color: color-mix(in srgb, $secondary-color 10%, transparent);
+        color: $secondary-color;
+      }
+    }
+
+    &.faulty {
+      &::before {
+        background-color: $error-color;
+      }
+
+      .stat-icon {
+        background-color: color-mix(in srgb, $error-color 10%, transparent);
+        color: $error-color;
+      }
+    }
+
+    &.repairing {
+      &::before {
+        background-color: $warning-color;
+      }
+
+      .stat-icon {
+        background-color: color-mix(in srgb, $warning-color 10%, transparent);
+        color: $warning-color;
+      }
+    }
+
+    &.value {
+      &::before {
+        background-color: $success-color;
+      }
+
+      .stat-icon {
+        background-color: color-mix(in srgb, $success-color 10%, transparent);
+        color: $success-color;
+      }
+    }
+  }
+}
+
+// 资产列表/网格视图
+.assets-view {
+  margin-bottom: 32px;
+}
+
+// 网格视图
+.assets-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+  gap: 20px;
+}
+
+.asset-card {
+  background-color: $bg-primary;
+  border-radius: $border-radius;
+  box-shadow: $shadow-sm;
+  padding: 20px;
+  transition: $transition;
+  position: relative;
+  overflow: hidden;
+
+  &:hover {
+    transform: translateY(-4px);
+    box-shadow: $shadow-md;
+  }
+
+  &.faulty {
+    animation: faulty-pulse 2s infinite;
+  }
+
+  .asset-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 16px;
+
+    .asset-type-icon {
+      width: 48px;
+      height: 48px;
+      border-radius: 50%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-size: 20px;
+      background-color: $bg-tertiary;
+      color: $primary-color;
+      transition: $transition;
+
+      &.电脑 {
+        background-color: color-mix(in srgb, $primary-color 10%, $bg-tertiary);
+        color: $primary-color;
+      }
+
+      &.外设 {
+        background-color: color-mix(in srgb, $secondary-color 10%, $bg-tertiary);
+        color: $secondary-color;
+      }
+
+      &.软件账号 {
+        background-color: color-mix(in srgb, $info-color 10%, $bg-tertiary);
+        color: $info-color;
+      }
+
+      &.域名 {
+        background-color: color-mix(in srgb, $warning-color 10%, $bg-tertiary);
+        color: $warning-color;
+      }
+
+      &.其他 {
+        background-color: color-mix(in srgb, $text-secondary 10%, $bg-tertiary);
+        color: $text-secondary;
+      }
+    }
+
+    .asset-status {
+      padding: 6px 12px;
+      border-radius: 16px;
+      font-size: 12px;
+      font-weight: 500;
+      text-align: center;
+      min-width: 60px;
+      transition: $transition;
+
+      &.status-idle {
+        background-color: color-mix(in srgb, $secondary-color 15%, transparent);
+        color: $secondary-color;
+      }
+
+      &.status-occupied {
+        background-color: color-mix(in srgb, $primary-color 15%, transparent);
+        color: $primary-color;
+      }
+
+      &.status-faulty {
+        background-color: color-mix(in srgb, $error-color 15%, transparent);
+        color: $error-color;
+      }
+
+      &.status-repairing {
+        background-color: color-mix(in srgb, $warning-color 15%, transparent);
+        color: $warning-color;
+      }
+    }
+  }
+
+  .asset-content {
+    margin-bottom: 16px;
+
+    .asset-name {
+      font-size: 18px;
+      font-weight: 600;
+      color: $text-primary;
+      margin: 0 0 12px 0;
+      line-height: 1.3;
+    }
+
+    .asset-info {
+      display: flex;
+      flex-direction: column;
+      gap: 8px;
+
+      .info-item {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        padding: 8px 0;
+        border-bottom: 1px solid $border-color;
+
+        .info-label {
+          font-size: 12px;
+          color: $text-tertiary;
+          font-weight: 500;
+        }
+
+        .info-value {
+          font-size: 12px;
+          color: $text-secondary;
+          font-weight: 500;
+        }
+      }
+
+      .info-item.faulty-notice {
+        background-color: color-mix(in srgb, $error-color 5%, transparent);
+        border-radius: 6px;
+        padding: 8px 12px;
+        border: none;
+
+        .info-value {
+          color: $error-color;
+        }
+      }
+    }
+  }
+
+  .asset-actions {
+    display: flex;
+    justify-content: flex-end;
+    gap: 8px;
+    padding-top: 16px;
+    border-top: 1px solid $border-color;
+
+    .action-btn {
+      width: 36px;
+      height: 36px;
+      border-radius: 50%;
+      color: $text-secondary;
+      background-color: $bg-tertiary;
+      transition: $transition;
+
+      &:hover {
+        background-color: $border-color;
+        color: $text-primary;
+        transform: scale(1.1);
+      }
+
+      &.primary {
+        color: $primary-color;
+        background-color: color-mix(in srgb, $primary-color 10%, $bg-tertiary);
+
+        &:hover {
+          background-color: $primary-color;
+          color: white;
+        }
+      }
+
+      &.warning {
+        color: $error-color;
+        background-color: color-mix(in srgb, $error-color 10%, $bg-tertiary);
+
+        &:hover {
+          background-color: $error-color;
+          color: white;
+        }
+      }
+    }
+  }
+}
+
+// 列表视图
+.assets-list {
+  background-color: $bg-primary;
+  border-radius: $border-radius;
+  box-shadow: $shadow-sm;
+  overflow: hidden;
+}
+
+.assets-table {
+  width: 100%;
+  min-width: 800px;
+
+  .mat-header-row {
+    background-color: $bg-tertiary;
+
+    th {
+      font-size: 12px;
+      font-weight: 600;
+      color: $text-secondary;
+      text-transform: uppercase;
+      letter-spacing: 0.5px;
+      padding: 12px 16px;
+      text-align: left;
+      border-bottom: 1px solid $border-color;
+    }
+  }
+
+  .mat-row {
+    transition: $transition;
+
+    &:hover {
+      background-color: $bg-tertiary;
+      transform: translateX(2px);
+    }
+
+    td {
+      padding: 12px 16px;
+      border-bottom: 1px solid $border-color;
+      font-size: 14px;
+      color: $text-primary;
+    }
+
+    &:last-child td {
+      border-bottom: none;
+    }
+  }
+
+  .asset-name-list {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+
+    .asset-type-icon-small {
+      width: 24px;
+      height: 24px;
+      border-radius: 50%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-size: 14px;
+      background-color: $bg-tertiary;
+      color: $primary-color;
+
+      &.电脑 {
+        background-color: color-mix(in srgb, $primary-color 10%, $bg-tertiary);
+        color: $primary-color;
+      }
+
+      &.外设 {
+        background-color: color-mix(in srgb, $secondary-color 10%, $bg-tertiary);
+        color: $secondary-color;
+      }
+
+      &.软件账号 {
+        background-color: color-mix(in srgb, $info-color 10%, $bg-tertiary);
+        color: $info-color;
+      }
+
+      &.域名 {
+        background-color: color-mix(in srgb, $warning-color 10%, $bg-tertiary);
+        color: $warning-color;
+      }
+
+      &.其他 {
+        background-color: color-mix(in srgb, $text-secondary 10%, $bg-tertiary);
+        color: $text-secondary;
+      }
+    }
+  }
+
+  .status-badge {
+    padding: 4px 12px;
+    border-radius: 16px;
+    font-size: 12px;
+    font-weight: 500;
+    text-align: center;
+    display: inline-block;
+    transition: $transition;
+
+    &.status-idle {
+      background-color: color-mix(in srgb, $secondary-color 15%, transparent);
+      color: $secondary-color;
+    }
+
+    &.status-occupied {
+      background-color: color-mix(in srgb, $primary-color 15%, transparent);
+      color: $primary-color;
+    }
+
+    &.status-faulty {
+      background-color: color-mix(in srgb, $error-color 15%, transparent);
+      color: $error-color;
+    }
+
+    &.status-repairing {
+      background-color: color-mix(in srgb, $warning-color 15%, transparent);
+      color: $warning-color;
+    }
+  }
+
+  .actions-column {
+    text-align: right;
+  }
+
+  .action-buttons-list {
+    display: flex;
+    justify-content: flex-end;
+    gap: 4px;
+
+    .action-btn {
+      width: 32px;
+      height: 32px;
+      border-radius: 50%;
+      color: $text-secondary;
+      background-color: $bg-tertiary;
+      transition: $transition;
+
+      &:hover {
+        background-color: $border-color;
+        color: $text-primary;
+        transform: scale(1.1);
+      }
+
+      &.primary {
+        color: $primary-color;
+        background-color: color-mix(in srgb, $primary-color 10%, $bg-tertiary);
+
+        &:hover {
+          background-color: $primary-color;
+          color: white;
+        }
+      }
+
+      &.warning {
+        color: $error-color;
+        background-color: color-mix(in srgb, $error-color 10%, $bg-tertiary);
+
+        &:hover {
+          background-color: $error-color;
+          color: white;
+        }
+      }
+    }
+  }
+
+  .no-data {
+    text-align: center;
+    padding: 60px 20px;
+
+    .empty-state {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+
+      mat-icon {
+        font-size: 48px;
+        color: $text-tertiary;
+        margin-bottom: 16px;
+      }
+
+      p {
+        color: $text-secondary;
+        font-size: 16px;
+        margin: 0;
+      }
+    }
+  }
+}
+
+// 统计图表区
+.stats-section {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
+  gap: 24px;
+}
+
+.stats-card {
+  background-color: $bg-primary;
+  border-radius: $border-radius;
+  box-shadow: $shadow-sm;
+  padding: 24px;
+  transition: $transition;
+
+  &:hover {
+    transform: translateY(-2px);
+    box-shadow: $shadow-md;
+  }
+
+  .card-header {
+    margin-bottom: 24px;
+
+    h2 {
+      font-size: 20px;
+      font-weight: 600;
+      color: $text-primary;
+      margin: 0;
+    }
+  }
+
+  .chart-content {
+    min-height: 200px;
+  }
+
+  // 类型统计
+  .type-stats {
+    display: flex;
+    flex-direction: column;
+    gap: 16px;
+
+    .type-stat-item {
+      display: flex;
+      align-items: center;
+      gap: 12px;
+
+      .type-info {
+        flex: 0 0 120px;
+
+        .type-name {
+          font-size: 14px;
+          font-weight: 500;
+          color: $text-primary;
+          display: block;
+        }
+
+        .type-count {
+          font-size: 12px;
+          color: $text-tertiary;
+          display: block;
+          margin-top: 2px;
+        }
+      }
+
+      .progress-bar {
+        flex: 1;
+        height: 12px;
+        background-color: $bg-tertiary;
+        border-radius: 6px;
+        overflow: hidden;
+        position: relative;
+
+        .progress-fill {
+          height: 100%;
+          background-color: $primary-color;
+          border-radius: 6px;
+          transition: width 0.8s ease-out;
+          position: relative;
+          overflow: hidden;
+
+          &::after {
+            content: '';
+            position: absolute;
+            top: 0;
+            left: 0;
+            right: 0;
+            bottom: 0;
+            background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
+            animation: progressAnimation 1.5s infinite;
+          }
+        }
+      }
+
+      .type-value {
+        font-size: 14px;
+        font-weight: 600;
+        color: $text-primary;
+        min-width: 100px;
+        text-align: right;
+      }
+    }
+  }
+
+  // 部门统计
+  .department-stats {
+    display: flex;
+    flex-direction: column;
+    gap: 16px;
+
+    .department-stat-item {
+      display: flex;
+      align-items: center;
+      gap: 12px;
+
+      .department-info {
+        flex: 0 0 150px;
+
+        .department-name {
+          font-size: 14px;
+          font-weight: 500;
+          color: $text-primary;
+          display: block;
+        }
+
+        .department-count {
+          font-size: 12px;
+          color: $text-tertiary;
+          display: block;
+          margin-top: 2px;
+        }
+      }
+
+      .progress-bar {
+        flex: 1;
+        height: 12px;
+        background-color: $bg-tertiary;
+        border-radius: 6px;
+        overflow: hidden;
+        position: relative;
+
+        .progress-fill {
+          height: 100%;
+          background-color: $secondary-color;
+          border-radius: 6px;
+          transition: width 0.8s ease-out;
+          position: relative;
+          overflow: hidden;
+
+          &::after {
+            content: '';
+            position: absolute;
+            top: 0;
+            left: 0;
+            right: 0;
+            bottom: 0;
+            background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
+            animation: progressAnimation 1.5s infinite;
+          }
+        }
+      }
+
+      .department-value {
+        font-size: 14px;
+        font-weight: 600;
+        color: $text-primary;
+        min-width: 100px;
+        text-align: right;
+      }
+    }
+  }
+
+  // 使用时长统计
+  .usage-stats {
+    display: flex;
+    align-items: flex-end;
+    gap: 16px;
+    height: 200px;
+    padding-top: 20px;
+
+    .usage-stat-item {
+      flex: 1;
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      gap: 8px;
+
+      .usage-info {
+        text-align: center;
+
+        .usage-type {
+          font-size: 12px;
+          color: $text-secondary;
+          font-weight: 500;
+        }
+      }
+
+      .bar-container {
+        position: relative;
+        width: 100%;
+        height: 100%;
+        display: flex;
+        align-items: flex-end;
+        justify-content: center;
+      }
+
+      .usage-bar {
+        width: 40px;
+        background-color: $primary-color;
+        border-radius: 4px 4px 0 0;
+        position: relative;
+        overflow: hidden;
+        transition: height 1s ease-out;
+        cursor: pointer;
+
+        &::after {
+          content: '';
+          position: absolute;
+          top: 0;
+          left: 0;
+          right: 0;
+          bottom: 0;
+          background: linear-gradient(180deg, rgba(255, 255, 255, 0.2), transparent);
+        }
+
+        &:hover {
+          background-color: $primary-light;
+          transform: scaleY(1.02);
+        }
+
+        .usage-value {
+          position: absolute;
+          top: -24px;
+          left: 50%;
+          transform: translateX(-50%);
+          font-size: 12px;
+          font-weight: 600;
+          color: $text-primary;
+        }
+      }
+    }
+  }
+}
+
+// 动画定义
+@keyframes faulty-pulse {
+  0%, 100% {
+    box-shadow: $shadow-sm, 0 0 0 0 rgba(239, 68, 68, 0);
+  }
+  50% {
+    box-shadow: $shadow-md, 0 0 0 4px rgba(239, 68, 68, 0.3);
+  }
+}
+
+@keyframes progressAnimation {
+  0% {
+    transform: translateX(-100%);
+  }
+  100% {
+    transform: translateX(100%);
+  }
+}
+
+// 响应式设计
+@media (max-width: 1200px) {
+  .assets-stats-container {
+    padding: 16px;
+  }
+
+  .stats-section {
+    grid-template-columns: 1fr;
+  }
+
+  .assets-grid {
+    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
+  }
+}
+
+@media (max-width: 768px) {
+  .assets-stats-container {
+    padding: 12px;
+  }
+
+  .page-header {
+    margin-bottom: 20px;
+
+    h1 {
+      font-size: 24px;
+    }
+  }
+
+  .filter-bar {
+    padding: 16px;
+  }
+
+  .filter-controls {
+    flex-direction: column;
+    align-items: stretch;
+
+    .filter-group {
+      min-width: auto;
+    }
+  }
+
+  .view-toggle {
+    flex-wrap: wrap;
+    justify-content: center;
+
+    .asset-count {
+      margin-left: 0;
+      width: 100%;
+      text-align: center;
+    }
+  }
+
+  .stats-cards {
+    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+    gap: 12px;
+  }
+
+  .asset-card {
+    padding: 16px;
+
+    .asset-header {
+      flex-direction: column;
+      align-items: flex-start;
+      gap: 12px;
+    }
+
+    .asset-actions {
+      justify-content: center;
+    }
+  }
+
+  .assets-list {
+    overflow-x: auto;
+  }
+
+  .stats-section {
+    gap: 16px;
+  }
+
+  .stats-card {
+    padding: 16px;
+  }
+
+  .usage-stats {
+    height: 150px;
+    gap: 8px;
+  }
+
+  .usage-bar {
+    width: 24px;
+  }
+}
+
+@media (max-width: 480px) {
+  .stats-cards {
+    grid-template-columns: 1fr 1fr;
+  }
+
+  .assets-grid {
+    grid-template-columns: 1fr;
+  }
+}

+ 823 - 0
src/app/pages/hr/assets/assets-stats.ts

@@ -0,0 +1,823 @@
+import { Component, OnInit, signal, computed, Inject } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { MatButtonModule } from '@angular/material/button';
+import { MatCardModule } from '@angular/material/card';
+import { MatIconModule } from '@angular/material/icon';
+import { MatDialogModule, MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { MatTabsModule } from '@angular/material/tabs';
+import { MatTooltipModule } from '@angular/material/tooltip';
+import { MatTableModule } from '@angular/material/table';
+import { Asset, AssetType, AssetStatus, AssetAssignment, Employee } from '../../../models/hr.model';
+
+// 资产分配对话框组件
+@Component({
+  selector: 'app-asset-assignment-dialog',
+  standalone: true,
+  imports: [
+    CommonModule,
+    FormsModule,
+    MatButtonModule,
+    MatIconModule
+  ],
+  template: `
+    <div class="dialog-header">
+      <h2>{{ isEdit ? '修改资产分配' : '分配资产' }}</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="info-item">
+        <label>资产名称:</label>
+        <span>{{ asset.name }}</span>
+      </div>
+      <div class="form-group">
+        <label>选择员工:</label>
+        <input 
+          type="text" 
+          [(ngModel)]="selectedEmployeeName"
+          (input)="filterEmployees($event.target.value)"
+          placeholder="搜索员工姓名或工号..."
+          class="employee-search"
+          [disabled]="isReturning"
+        >
+        <div class="employee-dropdown" *ngIf="showEmployeeDropdown && !isReturning">
+          <div *ngFor="let employee of filteredEmployees" 
+               class="employee-item" 
+               (click)="selectEmployee(employee)">
+            <span class="employee-name">{{ employee.name }}</span>
+            <span class="employee-id">{{ employee.employeeId }}</span>
+          </div>
+        </div>
+      </div>
+      <div class="form-group" *ngIf="!isReturning">
+        <label>分配开始日期:</label>
+        <input type="date" [(ngModel)]="startDate" class="date-input">
+      </div>
+      <div class="form-group" *ngIf="!isReturning">
+        <label>预计归还日期:</label>
+        <input type="date" [(ngModel)]="endDate" class="date-input">
+      </div>
+      <div class="form-group" *ngIf="isReturning">
+        <label>实际归还日期:</label>
+        <input type="date" [(ngModel)]="returnDate" class="date-input">
+      </div>
+      <div class="form-group">
+        <label>备注:</label>
+        <textarea 
+          [(ngModel)]="notes" 
+          placeholder="请输入备注信息..." 
+          rows="3"
+          class="notes-input"
+        ></textarea>
+      </div>
+    </div>
+    <div class="dialog-actions">
+      <button mat-button (click)="dialogRef.close()">取消</button>
+      <button mat-raised-button color="primary" (click)="submit()">
+        {{ isReturning ? '确认归还' : '确认分配' }}
+      </button>
+    </div>
+  `,
+  styles: [`
+    .dialog-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 24px;
+      padding-bottom: 16px;
+      border-bottom: 1px solid #e5e7eb;
+    }
+    .close-btn {
+      background: none;
+      border: none;
+      cursor: pointer;
+      color: #6b7280;
+      padding: 4px;
+    }
+    .dialog-content {
+      max-width: 500px;
+    }
+    .info-item {
+      display: flex;
+      justify-content: space-between;
+      margin-bottom: 16px;
+      padding: 8px 0;
+      border-bottom: 1px solid #f3f4f6;
+    }
+    .form-group {
+      margin-bottom: 20px;
+      position: relative;
+    }
+    label {
+      display: block;
+      margin-bottom: 4px;
+      font-weight: 500;
+      color: #374151;
+    }
+    .employee-search,
+    .date-input {
+      width: 100%;
+      padding: 8px 12px;
+      border: 1px solid #e5e7eb;
+      border-radius: 6px;
+      font-size: 14px;
+    }
+    .employee-dropdown {
+      position: absolute;
+      top: 100%;
+      left: 0;
+      right: 0;
+      background-color: white;
+      border: 1px solid #e5e7eb;
+      border-radius: 6px;
+      max-height: 200px;
+      overflow-y: auto;
+      box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+      z-index: 1000;
+    }
+    .employee-item {
+      padding: 10px 12px;
+      cursor: pointer;
+      transition: background-color 0.2s;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+    }
+    .employee-item:hover {
+      background-color: #f3f4f6;
+    }
+    .employee-name {
+      font-weight: 500;
+      color: #1f2937;
+    }
+    .employee-id {
+      font-size: 12px;
+      color: #6b7280;
+    }
+    .notes-input {
+      width: 100%;
+      padding: 8px 12px;
+      border: 1px solid #e5e7eb;
+      border-radius: 6px;
+      font-size: 14px;
+      resize: vertical;
+    }
+    .dialog-actions {
+      display: flex;
+      justify-content: flex-end;
+      gap: 12px;
+      margin-top: 24px;
+    }
+  `]
+}) class AssetAssignmentDialog {
+  asset: Asset;
+  employees: Employee[] = [];
+  filteredEmployees: Employee[] = [];
+  showEmployeeDropdown = false;
+  selectedEmployeeId = '';
+  selectedEmployeeName = '';
+  startDate = new Date().toISOString().split('T')[0];
+  endDate = '';
+  returnDate = new Date().toISOString().split('T')[0];
+  notes = '';
+  isEdit = false;
+  isReturning = false;
+  
+  constructor(
+    public dialogRef: MatDialogRef<AssetAssignmentDialog>,
+    @Inject(MAT_DIALOG_DATA) public data: any
+  ) {
+    this.asset = data.asset;
+    this.employees = data.employees;
+    this.filteredEmployees = [...this.employees];
+    this.isEdit = data.isEdit || false;
+    this.isReturning = data.isReturning || false;
+    
+    if (this.isEdit && data.assignment) {
+      const assignment = data.assignment;
+      const employee = this.employees.find(e => e.id === assignment.employeeId);
+      if (employee) {
+        this.selectedEmployeeId = employee.id;
+        this.selectedEmployeeName = employee.name;
+      }
+      if (assignment.startDate) {
+        this.startDate = new Date(assignment.startDate).toISOString().split('T')[0];
+      }
+      if (assignment.endDate) {
+        this.endDate = new Date(assignment.endDate).toISOString().split('T')[0];
+      }
+    }
+  }
+  
+  filterEmployees(query: string) {
+    if (!query.trim()) {
+      this.filteredEmployees = [...this.employees];
+    } else {
+      const term = query.toLowerCase();
+      this.filteredEmployees = this.employees.filter(emp => 
+        emp.name.toLowerCase().includes(term) ||
+        emp.employeeId.toLowerCase().includes(term)
+      );
+    }
+    this.showEmployeeDropdown = true;
+  }
+  
+  selectEmployee(employee: Employee) {
+    this.selectedEmployeeId = employee.id;
+    this.selectedEmployeeName = employee.name;
+    this.showEmployeeDropdown = false;
+  }
+  
+  submit() {
+    if (!this.selectedEmployeeId && !this.isReturning) {
+      alert('请选择员工');
+      return;
+    }
+    
+    this.dialogRef.close({
+      employeeId: this.selectedEmployeeId,
+      startDate: this.startDate,
+      endDate: this.isReturning ? this.returnDate : this.endDate,
+      notes: this.notes
+    });
+  }
+}
+
+// 报修对话框组件
+@Component({
+  selector: 'app-asset-repair-dialog',
+  standalone: true,
+  imports: [
+    CommonModule,
+    FormsModule,
+    MatButtonModule,
+    MatIconModule
+  ],
+  template: `
+    <div class="dialog-header">
+      <h2>资产报修</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="info-item">
+        <label>资产名称:</label>
+        <span>{{ asset.name }}</span>
+      </div>
+      <div class="info-item">
+        <label>资产类型:</label>
+        <span>{{ asset.type }}</span>
+      </div>
+      <div class="info-item">
+        <label>序列号:</label>
+        <span>{{ asset.serialNumber || '无' }}</span>
+      </div>
+      <div class="form-group">
+        <label>故障描述 *:</label>
+        <textarea 
+          [(ngModel)]="problemDescription" 
+          placeholder="请详细描述故障情况..." 
+          rows="4"
+          class="description-input"
+          required
+        ></textarea>
+      </div>
+      <div class="form-group">
+        <label>期望修复时间:</label>
+        <input type="date" [(ngModel)]="expectedFixDate" class="date-input">
+      </div>
+      <div class="form-group">
+        <label>联系人:</label>
+        <input type="text" [(ngModel)]="contactPerson" class="contact-input" placeholder="请输入联系人姓名">
+      </div>
+      <div class="form-group">
+        <label>联系电话:</label>
+        <input type="text" [(ngModel)]="contactPhone" class="phone-input" placeholder="请输入联系电话">
+      </div>
+    </div>
+    <div class="dialog-actions">
+      <button mat-button (click)="dialogRef.close()">取消</button>
+      <button mat-raised-button color="primary" (click)="submit()" [disabled]="!problemDescription.trim()">
+        提交报修申请
+      </button>
+    </div>
+  `,
+  styles: [`
+    .dialog-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 24px;
+      padding-bottom: 16px;
+      border-bottom: 1px solid #e5e7eb;
+    }
+    .close-btn {
+      background: none;
+      border: none;
+      cursor: pointer;
+      color: #6b7280;
+      padding: 4px;
+    }
+    .dialog-content {
+      max-width: 500px;
+    }
+    .info-item {
+      display: flex;
+      justify-content: space-between;
+      margin-bottom: 16px;
+      padding: 8px 0;
+      border-bottom: 1px solid #f3f4f6;
+    }
+    .form-group {
+      margin-bottom: 20px;
+    }
+    label {
+      display: block;
+      margin-bottom: 4px;
+      font-weight: 500;
+      color: #374151;
+    }
+    .description-input,
+    .date-input,
+    .contact-input,
+    .phone-input {
+      width: 100%;
+      padding: 8px 12px;
+      border: 1px solid #e5e7eb;
+      border-radius: 6px;
+      font-size: 14px;
+    }
+    .description-input {
+      resize: vertical;
+    }
+    .dialog-actions {
+      display: flex;
+      justify-content: flex-end;
+      gap: 12px;
+      margin-top: 24px;
+    }
+  `]
+}) class AssetRepairDialog {
+  asset: Asset;
+  problemDescription = '';
+  expectedFixDate = new Date().toISOString().split('T')[0];
+  contactPerson = '';
+  contactPhone = '';
+  
+  constructor(
+    public dialogRef: MatDialogRef<AssetRepairDialog>,
+    @Inject(MAT_DIALOG_DATA) public data: any
+  ) {
+    this.asset = data.asset;
+  }
+  
+  submit() {
+    if (!this.problemDescription.trim()) {
+      alert('请填写故障描述');
+      return;
+    }
+    
+    this.dialogRef.close({
+      problemDescription: this.problemDescription,
+      expectedFixDate: this.expectedFixDate,
+      contactPerson: this.contactPerson,
+      contactPhone: this.contactPhone
+    });
+  }
+}
+
+// 生成模拟资产数据
+const generateMockAssets = (): Asset[] => {
+  const assets: Asset[] = [];
+  const types: AssetType[] = ['电脑', '外设', '软件账号', '域名', '其他'];
+  const statuses: AssetStatus[] = ['空闲', '占用', '故障', '报修中'];
+  const departments = ['设计部', '客户服务部', '财务部', '人力资源部'];
+  const computerModels = ['MacBook Pro', 'Dell XPS', 'HP EliteBook', 'Lenovo ThinkPad', 'Surface Laptop'];
+  const peripheralTypes = ['显示器', '键盘', '鼠标', '打印机', '扫描仪', '投影仪'];
+  const softwareTypes = ['Adobe Creative Cloud', 'AutoCAD', 'Office 365', '渲染农场账号', '素材库账号'];
+  const otherAssets = ['办公桌', '办公椅', '服务器', '网络设备', '空调'];
+  
+  for (let i = 1; i <= 30; i++) {
+    const type = types[Math.floor(Math.random() * types.length)];
+    const status = statuses[Math.floor(Math.random() * statuses.length)];
+    const purchaseDate = new Date();
+    purchaseDate.setMonth(purchaseDate.getMonth() - Math.floor(Math.random() * 36));
+    
+    let name = '';
+    let serialNumber = `SN${Math.floor(Math.random() * 1000000)}`;
+    
+    switch (type) {
+      case '电脑':
+        name = `${computerModels[Math.floor(Math.random() * computerModels.length)]} ${2020 + Math.floor(Math.random() * 4)}`;
+        break;
+      case '外设':
+        name = peripheralTypes[Math.floor(Math.random() * peripheralTypes.length)];
+        break;
+      case '软件账号':
+        name = softwareTypes[Math.floor(Math.random() * softwareTypes.length)];
+        serialNumber = '无';
+        break;
+      case '域名':
+        name = `example-${i}.com`;
+        serialNumber = '无';
+        break;
+      case '其他':
+        name = otherAssets[Math.floor(Math.random() * otherAssets.length)];
+        break;
+    }
+    
+    const value = Math.floor(Math.random() * 10000) + 500;
+    const assignedTo = status === '占用' ? `emp-${Math.floor(Math.random() * 10) + 1}` : undefined;
+    const assignedToName = assignedTo ? `员工${Math.floor(Math.random() * 20) + 1}` : undefined;
+    
+    // 随机设置保修截止日期
+    const warrantyExpiry = new Date(purchaseDate);
+    warrantyExpiry.setFullYear(warrantyExpiry.getFullYear() + (Math.random() > 0.5 ? 1 : 2));
+    
+    assets.push({
+      id: `asset-${i}`,
+      name,
+      type,
+      status,
+      purchaseDate,
+      value,
+      assignedTo,
+      assignedToName,
+      department: departments[Math.floor(Math.random() * departments.length)],
+      description: `这是一台${name},用于日常办公和项目开发。`,
+      serialNumber,
+      warrantyExpiry
+    });
+  }
+  
+  return assets;
+};
+
+// 生成模拟员工数据
+const generateMockEmployees = (): Employee[] => {
+  const employees: Employee[] = [];
+  const departments = ['设计部', '客户服务部', '财务部', '人力资源部'];
+  const positions = ['设计师', '客服专员', '财务专员', '人事专员', '经理', '助理'];
+  const names = ['张三', '李四', '王五', '赵六', '钱七', '孙八', '周九', '吴十'];
+  
+  for (let i = 1; i <= 20; i++) {
+    employees.push({
+      id: `emp-${i}`,
+      name: names[i % names.length] + i,
+      department: departments[Math.floor(Math.random() * departments.length)],
+      position: positions[Math.floor(Math.random() * positions.length)],
+      employeeId: `EMP2023${String(i).padStart(3, '0')}`,
+      phone: `138${Math.floor(Math.random() * 100000000)}`,
+      email: `employee${i}@example.com`,
+      gender: i % 2 === 0 ? '女' : '男',
+      birthDate: new Date(1980 + Math.floor(Math.random() * 20), Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1),
+      hireDate: new Date(2020 + Math.floor(Math.random() * 3), Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1),
+      status: '在职'
+    });
+  }
+  
+  return employees;
+};
+
+// 生成模拟资产分配记录
+const generateMockAssignments = (): AssetAssignment[] => {
+  const assignments: AssetAssignment[] = [];
+  
+  for (let i = 1; i <= 15; i++) {
+    const startDate = new Date();
+    startDate.setMonth(startDate.getMonth() - Math.floor(Math.random() * 6));
+    
+    const endDate = Math.random() > 0.5 ? new Date(startDate) : undefined;
+    if (endDate) {
+      endDate.setMonth(endDate.getMonth() + Math.floor(Math.random() * 3) + 1);
+    }
+    
+    assignments.push({
+      id: `assignment-${i}`,
+      assetId: `asset-${Math.floor(Math.random() * 20) + 1}`,
+      employeeId: `emp-${Math.floor(Math.random() * 15) + 1}`,
+      startDate,
+      endDate,
+      status: endDate ? '已归还' : '进行中'
+    });
+  }
+  
+  return assignments;
+};
+
+// 主组件
+@Component({
+  selector: 'app-assets-stats',
+  standalone: true,
+  imports: [
+    CommonModule,
+    FormsModule,
+    MatButtonModule,
+    MatCardModule,
+    MatIconModule,
+    MatDialogModule,
+    MatTabsModule,
+    MatTooltipModule,
+    MatTableModule
+  ],
+  templateUrl: './assets-stats.html',
+  styleUrl: './assets-stats.scss'
+}) export class AssetsStats implements OnInit {
+  // 暴露Math对象给模板使用
+  readonly Math = Math;
+  
+  // 数据
+  assets = signal<Asset[]>([]);
+  employees = signal<Employee[]>([]);
+  assignments = signal<AssetAssignment[]>([]);
+  selectedView = signal<'grid' | 'list'>('grid');
+  searchTerm = signal('');
+  typeFilter = signal<AssetType | ''>('');
+  statusFilter = signal<AssetStatus | ''>('');
+  departmentFilter = signal('');
+  
+  // 计算属性
+  filteredAssets = computed(() => {
+    let filtered = this.assets();
+    
+    // 按搜索词筛选
+    if (this.searchTerm()) {
+      const term = this.searchTerm().toLowerCase();
+      filtered = filtered.filter(asset => 
+        asset.name.toLowerCase().includes(term) ||
+        (asset.serialNumber && asset.serialNumber.toLowerCase().includes(term)) ||
+        (asset.department && asset.department.toLowerCase().includes(term)) ||
+        (asset.assignedToName && asset.assignedToName.toLowerCase().includes(term))
+      );
+    }
+    
+    // 按类型筛选
+    if (this.typeFilter()) {
+      filtered = filtered.filter(asset => asset.type === this.typeFilter());
+    }
+    
+    // 按状态筛选
+    if (this.statusFilter()) {
+      filtered = filtered.filter(asset => asset.status === this.statusFilter());
+    }
+    
+    // 按部门筛选
+    if (this.departmentFilter()) {
+      filtered = filtered.filter(asset => asset.department === this.departmentFilter());
+    }
+    
+    return filtered;
+  });
+  
+  // 资产统计
+  assetStats = computed(() => {
+    const total = this.assets().length;
+    const occupied = this.assets().filter(a => a.status === '占用').length;
+    const idle = this.assets().filter(a => a.status === '空闲').length;
+    const faulty = this.assets().filter(a => a.status === '故障').length;
+    const repairing = this.assets().filter(a => a.status === '报修中').length;
+    
+    const totalValue = this.assets().reduce((sum, asset) => sum + asset.value, 0);
+    
+    // 按类型统计
+    const typeStats = new Map<AssetType, { count: number, value: number }>();
+    this.assets().forEach(asset => {
+      const current = typeStats.get(asset.type) || { count: 0, value: 0 };
+      current.count++;
+      current.value += asset.value;
+      typeStats.set(asset.type, current);
+    });
+    
+    // 转换为数组格式以便在模板中使用
+    const typeStatsArray = Array.from(typeStats.entries()).map(([key, value]) => ({
+      type: key,
+      ...value
+    }));
+    
+    // 按部门统计
+    const departmentStats = new Map<string, { count: number, value: number }>();
+    this.assets().forEach(asset => {
+      const department = asset.department || '未知部门'; // 提供默认值
+      const current = departmentStats.get(department) || { count: 0, value: 0 };
+      current.count++;
+      current.value += asset.value;
+      departmentStats.set(department, current);
+    });
+    
+    // 转换为数组格式以便在模板中使用
+    const departmentStatsArray = Array.from(departmentStats.entries()).map(([key, value]) => ({
+      department: key,
+      ...value
+    }));
+    
+    // 使用时长统计(模拟数据)
+    const usageStats = [
+      { type: '电脑', avgHours: Math.floor(Math.random() * 40) + 100 },
+      { type: '外设', avgHours: Math.floor(Math.random() * 30) + 80 },
+      { type: '软件账号', avgHours: Math.floor(Math.random() * 50) + 120 },
+      { type: '域名', avgHours: Math.floor(Math.random() * 20) + 60 },
+      { type: '其他', avgHours: Math.floor(Math.random() * 20) + 40 }
+    ];
+    
+    return {
+      total,
+      occupied,
+      idle,
+      faulty,
+      repairing,
+      totalValue,
+      typeStats,
+      typeStatsArray,
+      departmentStats,
+      departmentStatsArray,
+      usageStats
+    };
+  });
+  
+  // 获取资产类型列表
+  assetTypes = computed(() => {
+    return Array.from(new Set(this.assets().map(asset => asset.type)));
+  });
+  
+  // 获取部门列表
+  departments = computed(() => {
+    return Array.from(new Set(this.assets().map(asset => asset.department)));
+  });
+  
+  constructor(private dialog: MatDialog) {}
+  
+  ngOnInit() {
+    // 加载模拟数据
+    this.assets.set(generateMockAssets());
+    this.employees.set(generateMockEmployees());
+    this.assignments.set(generateMockAssignments());
+  }
+  
+  // 切换视图(网格/列表)
+  switchView(view: 'grid' | 'list') {
+    this.selectedView.set(view);
+  }
+  
+  // 格式化日期
+  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')}`;
+  }
+  
+  // 格式化金额
+  formatCurrency(amount: number): string {
+    return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(amount);
+  }
+  
+  // 获取状态样式类
+  getStatusClass(status: string): string {
+    switch (status) {
+      case '空闲':
+        return 'status-idle';
+      case '占用':
+        return 'status-occupied';
+      case '故障':
+        return 'status-faulty';
+      case '报修中':
+        return 'status-repairing';
+      default:
+        return '';
+    }
+  }
+  
+  // 获取类型图标
+  getTypeIcon(type: AssetType): string {
+    switch (type) {
+      case '电脑':
+        return 'laptop';
+      case '外设':
+        return 'devices';
+      case '软件账号':
+        return 'cloud';
+      case '域名':
+        return 'link';
+      case '其他':
+        return 'category';
+      default:
+        return 'help';
+    }
+  }
+  
+  // 打开资产分配对话框
+  openAssignmentDialog(asset: Asset, isReturning: boolean = false) {
+    let assignment: AssetAssignment | undefined;
+    if (isReturning) {
+      assignment = this.assignments().find(a => a.assetId === asset.id && a.status === '进行中');
+    }
+    
+    const dialogRef = this.dialog.open(AssetAssignmentDialog, {
+      width: '500px',
+      maxWidth: '90vw',
+      disableClose: true,
+      data: {
+        asset,
+        employees: this.employees(),
+        isEdit: !!assignment,
+        isReturning,
+        assignment
+      }
+    });
+    
+    dialogRef.afterClosed().subscribe(result => {
+      if (result) {
+        if (isReturning && assignment) {
+          // 更新归还信息
+          this.assignments.update(assignments => 
+            assignments.map(a => 
+              a.id === assignment!.id ? { ...a, endDate: result.endDate, status: '已归还' } : a
+            )
+          );
+          
+          // 更新资产状态为空闲
+          this.assets.update(assets => 
+            assets.map(a => 
+              a.id === asset.id ? { ...a, status: '空闲', assignedTo: undefined, assignedToName: undefined } : a
+            )
+          );
+        } else {
+          // 创建新分配记录
+          const newAssignment: AssetAssignment = {
+            id: `assignment-${Date.now()}`,
+            assetId: asset.id,
+            employeeId: result.employeeId,
+            startDate: new Date(result.startDate),
+            endDate: result.endDate ? new Date(result.endDate) : undefined,
+            status: '进行中'
+          };
+          
+          this.assignments.update(assignments => [newAssignment, ...assignments]);
+          
+          // 更新资产状态为占用
+          const employee = this.employees().find(e => e.id === result.employeeId);
+          this.assets.update(assets => 
+            assets.map(a => 
+              a.id === asset.id ? { ...a, status: '占用', assignedTo: result.employeeId, assignedToName: employee?.name } : a
+            )
+          );
+        }
+        alert(isReturning ? '资产归还成功' : '资产分配成功');
+      }
+    });
+  }
+  
+  // 打开报修对话框
+  openRepairDialog(asset: Asset) {
+    const dialogRef = this.dialog.open(AssetRepairDialog, {
+      width: '500px',
+      maxWidth: '90vw',
+      disableClose: true,
+      data: { asset }
+    });
+    
+    dialogRef.afterClosed().subscribe(result => {
+      if (result) {
+        // 更新资产状态为报修中
+        this.assets.update(assets => 
+          assets.map(a => 
+            a.id === asset.id ? { ...a, status: '报修中' } : a
+          )
+        );
+        alert('报修申请已提交');
+      }
+    });
+  }
+  
+  // 导出资产台账
+  exportAssetLedger() {
+    alert('资产台账导出功能待实现');
+  }
+  
+  // 重置筛选条件
+  resetFilters() {
+    this.searchTerm.set('');
+    this.typeFilter.set('');
+    this.statusFilter.set('');
+    this.departmentFilter.set('');
+  }
+  
+  // 获取资产使用情况
+  getAssetUsage(assetId: string): AssetAssignment | undefined {
+    return this.assignments().find(a => a.assetId === assetId && a.status === '进行中');
+  }
+  
+  // 获取员工姓名
+  getEmployeeName(employeeId: string): string {
+    const employee = this.employees().find(e => e.id === employeeId);
+    return employee ? employee.name : '未知员工';
+  }
+}

+ 224 - 1
src/app/pages/hr/assets/assets.html

@@ -1 +1,224 @@
-<p>assets works!</p>
+<div class="assets-container">
+  <header class="page-header">
+    <h1>花名册与档案库</h1>
+    <p class="page-description">管理员工全维度信息,包括基本信息、合同管理和证件管理</p>
+  </header>
+
+  <!-- 顶部操作栏 -->
+  <div class="action-bar">
+    <div class="action-buttons">
+      <button mat-raised-button color="primary" class="add-btn" (click)="openAddEmployeeDialog()">
+        <mat-icon>add</mat-icon>
+        新增员工
+      </button>
+      
+      <div class="batch-actions" [class.hidden]="selectedEmployees().length === 0">
+        <button mat-button color="warn" (click)="batchDelete()" class="batch-btn">
+          <mat-icon>delete</mat-icon>
+          批量删除
+        </button>
+        <div class="export-dropdown">
+          <button mat-button class="batch-btn" [matMenuTriggerFor]="exportMenu">
+            <mat-icon>file_download</mat-icon>
+            导出
+            <mat-icon>expand_more</mat-icon>
+          </button>
+          <mat-menu #exportMenu="matMenu">
+            <button mat-menu-item (click)="exportData('excel')">
+              <mat-icon>insert_drive_file</mat-icon>
+              Excel
+            </button>
+            <button mat-menu-item (click)="exportData('pdf')">
+              <mat-icon>picture_as_pdf</mat-icon>
+              PDF
+            </button>
+          </mat-menu>
+        </div>
+      </div>
+    </div>
+    
+    <div class="search-filters">
+      <div class="search-container">
+        <mat-icon class="search-icon">search</mat-icon>
+        <input 
+          matInput 
+          placeholder="搜索员工姓名、工号、手机号或邮箱..."
+          [value]="searchTerm()"
+          (input)="searchTerm.set($event.target.value)"
+          (keyup.enter)="applyFilters()"
+          class="search-input"
+        >
+        <button mat-icon-button *ngIf="searchTerm()" (click)="searchTerm.set('')" class="clear-search">
+          <mat-icon>close</mat-icon>
+        </button>
+      </div>
+      
+      <div class="filter-container">
+        <mat-select placeholder="部门" [value]="departmentFilter()" (selectionChange)="departmentFilter.set($event.value); applyFilters()">
+          <mat-option value="">全部部门</mat-option>
+          <mat-option *ngFor="let dept of departments" [value]="dept.name">{{ dept.name }}</mat-option>
+        </mat-select>
+      </div>
+      
+      <div class="filter-container">
+        <mat-select placeholder="状态" [value]="statusFilter()" (selectionChange)="statusFilter.set($event.value); applyFilters()">
+          <mat-option value="">全部状态</mat-option>
+          <mat-option *ngFor="let status of statuses" [value]="status">{{ status }}</mat-option>
+        </mat-select>
+      </div>
+      
+      <button mat-raised-button (click)="applyFilters()" class="filter-btn">
+        <mat-icon>filter_list</mat-icon>
+        筛选
+      </button>
+    </div>
+  </div>
+
+  <!-- 数据表格 -->
+  <div class="table-container">
+    <table mat-table [dataSource]="filteredEmployees()" class="employee-table">
+      <!-- 复选框列 -->
+      <ng-container matColumnDef="select">
+        <th mat-header-cell *matHeaderCellDef>
+          <mat-checkbox 
+            [checked]="isAllSelected()"
+            (change)="toggleSelectAll()"
+            [indeterminate]="selectedEmployees().length > 0 && selectedEmployees().length < filteredEmployees().length"
+          ></mat-checkbox>
+        </th>
+        <td mat-cell *matCellDef="let employee">
+          <mat-checkbox 
+            [checked]="selectedEmployees().includes(employee.id)"
+            (change)="toggleEmployeeSelection(employee.id)"
+          ></mat-checkbox>
+        </td>
+      </ng-container>
+
+      <!-- 姓名列 -->
+      <ng-container matColumnDef="name">
+        <th mat-header-cell *matHeaderCellDef class="name-column">姓名</th>
+        <td mat-cell *matCellDef="let employee" class="name-column">
+          <div class="employee-info">
+            <img [src]="employee.avatar" alt="员工头像" class="employee-avatar">
+            <span class="employee-name">{{ employee.name }}</span>
+          </div>
+        </td>
+      </ng-container>
+
+      <!-- 工号列 -->
+      <ng-container matColumnDef="employeeId">
+        <th mat-header-cell *matHeaderCellDef>工号</th>
+        <td mat-cell *matCellDef="let employee">{{ employee.employeeId }}</td>
+      </ng-container>
+
+      <!-- 部门列 -->
+      <ng-container matColumnDef="department">
+        <th mat-header-cell *matHeaderCellDef>部门</th>
+        <td mat-cell *matCellDef="let employee">{{ employee.department }}</td>
+      </ng-container>
+
+      <!-- 岗位列 -->
+      <ng-container matColumnDef="position">
+        <th mat-header-cell *matHeaderCellDef>岗位</th>
+        <td mat-cell *matCellDef="let employee">{{ employee.position }}</td>
+      </ng-container>
+
+      <!-- 手机号列 -->
+      <ng-container matColumnDef="phone">
+        <th mat-header-cell *matHeaderCellDef>手机号</th>
+        <td mat-cell *matCellDef="let employee">{{ employee.phone }}</td>
+      </ng-container>
+
+      <!-- 邮箱列 -->
+      <ng-container matColumnDef="email">
+        <th mat-header-cell *matHeaderCellDef>邮箱</th>
+        <td mat-cell *matCellDef="let employee">{{ employee.email }}</td>
+      </ng-container>
+
+      <!-- 入职日期列 -->
+      <ng-container matColumnDef="hireDate">
+        <th mat-header-cell *matHeaderCellDef>入职日期</th>
+        <td mat-cell *matCellDef="let employee">{{ formatDate(employee.hireDate) }}</td>
+      </ng-container>
+
+      <!-- 状态列 -->
+      <ng-container matColumnDef="status">
+        <th mat-header-cell *matHeaderCellDef>状态</th>
+        <td mat-cell *matCellDef="let employee">
+          <mat-select [value]="employee.status" (selectionChange)="changeEmployeeStatus(employee, $event.value)" class="status-select">
+            <mat-option *ngFor="let status of statuses" [value]="status">{{ status }}</mat-option>
+          </mat-select>
+        </td>
+      </ng-container>
+
+      <!-- 合同列 -->
+      <ng-container matColumnDef="contract">
+        <th mat-header-cell *matHeaderCellDef>合同</th>
+        <td mat-cell *matCellDef="let employee">
+          <div *ngIf="employee.contract" class="contract-info">
+            <div class="contract-date">{{ formatDate(employee.contract.startDate) }} - {{ formatDate(employee.contract.endDate) }}</div>
+            <div *ngIf="employee.contract.isExpiringSoon" class="expiring-soon" matTooltip="合同即将到期">
+              ⚠️ 即将到期
+            </div>
+            <button mat-icon-button class="contract-btn" matTooltip="查看合同">
+              <mat-icon>description</mat-icon>
+            </button>
+          </div>
+          <div *ngIf="!employee.contract" class="no-contract">
+            无合同信息
+          </div>
+        </td>
+      </ng-container>
+
+      <!-- 操作列 -->
+      <ng-container matColumnDef="actions">
+        <th mat-header-cell *matHeaderCellDef>操作</th>
+        <td mat-cell *matCellDef="let employee" class="actions-column">
+          <div class="action-buttons">
+            <button mat-icon-button class="edit-btn" matTooltip="编辑" (click)="editEmployee(employee)">
+              <mat-icon>edit</mat-icon>
+            </button>
+            <button mat-icon-button class="delete-btn" matTooltip="删除" (click)="deleteEmployee(employee.id)">
+              <mat-icon>delete</mat-icon>
+            </button>
+            <button mat-icon-button class="view-btn" matTooltip="查看详情">
+              <mat-icon>visibility</mat-icon>
+            </button>
+          </div>
+        </td>
+      </ng-container>
+
+      <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
+      <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
+
+      <!-- 空数据状态 -->
+      <tr class="mat-row" *matNoDataRow>
+        <td class="mat-cell empty-state" [attr.colspan]="displayedColumns.length">
+          <div class="empty-icon">
+            <mat-icon>search_off</mat-icon>
+          </div>
+          <p>没有找到符合条件的员工</p>
+          <button mat-button (click)="searchTerm.set(''); departmentFilter.set(''); statusFilter.set(''); applyFilters()">
+            清除筛选条件
+          </button>
+        </td>
+      </tr>
+    </table>
+  </div>
+
+  <!-- 分页组件 -->
+  <div class="pagination">
+    <div class="pagination-info">
+      共 {{ filteredEmployees().length }} 条记录,当前显示第 {{ pageIndex * pageSize + 1 }} - 
+      {{ Math.min((pageIndex + 1) * pageSize, filteredEmployees().length) }} 条
+    </div>
+    <mat-paginator
+      [length]="filteredEmployees().length"
+      [pageSize]="pageSize"
+      [pageSizeOptions]="[10, 20, 50]"
+      [pageIndex]="pageIndex"
+      (page)="onPageChange($event)"
+      showFirstLastButtons
+    ></mat-paginator>
+  </div>
+</div>

+ 502 - 0
src/app/pages/hr/assets/assets.scss

@@ -0,0 +1,502 @@
+// 自定义主题
+$primary-color: #1e40af; // 深蓝主色,传递可靠感
+$primary-light: #3b82f6; // 浅蓝色,用于悬停效果
+$secondary-color: #0d9488; // 薄荷绿,作为强调色
+$success-color: #10b981; // 成功色
+$warning-color: #f59e0b; // 警告色(浅橙)
+$error-color: #ef4444; // 错误色
+$text-primary: #1f2937; // 主要文本色
+$text-secondary: #4b5563; // 次要文本色
+$text-tertiary: #9ca3af; // 辅助文本色
+$bg-primary: #ffffff; // 主背景色
+$bg-secondary: #f9fafb; // 次要背景色
+$bg-tertiary: #f3f4f6; // 辅助背景色
+$border-color: #e5e7eb; // 边框色
+$shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+$shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+$shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+$border-radius: 8px;
+$transition: all 0.2s ease;
+
+// 主容器样式
+.assets-container {
+  padding: 24px;
+  min-height: 100vh;
+  background-color: $bg-secondary;
+  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+}
+
+// 页面标题
+.page-header {
+  margin-bottom: 24px;
+  padding-bottom: 16px;
+  border-bottom: 2px solid $border-color;
+
+  h1 {
+    font-size: 28px;
+    font-weight: 700;
+    color: $text-primary;
+    margin: 0 0 8px 0;
+  }
+
+  .page-description {
+    font-size: 16px;
+    color: $text-secondary;
+    margin: 0;
+  }
+}
+
+// 顶部操作栏
+.action-bar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 24px;
+  padding: 16px 20px;
+  background-color: $bg-primary;
+  border-radius: $border-radius;
+  box-shadow: $shadow-sm;
+  flex-wrap: wrap;
+  gap: 16px;
+
+  .action-buttons {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    flex-wrap: wrap;
+  }
+
+  .add-btn {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    padding: 10px 20px;
+    background-color: $primary-color;
+    color: white;
+    border: none;
+    border-radius: $border-radius;
+    font-size: 16px;
+    font-weight: 500;
+    cursor: pointer;
+    transition: $transition;
+
+    &:hover {
+      background-color: $primary-light;
+      transform: translateY(-1px);
+      box-shadow: $shadow-md;
+    }
+
+    &:active {
+      transform: translateY(0);
+      box-shadow: $shadow-sm;
+    }
+  }
+
+  .batch-actions {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    animation: slideIn 0.3s ease-out;
+
+    &.hidden {
+      display: none;
+    }
+  }
+
+  .batch-btn {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    padding: 8px 12px;
+    border: 1px solid $border-color;
+    border-radius: $border-radius;
+    background-color: $bg-primary;
+    color: $text-secondary;
+    font-size: 14px;
+    cursor: pointer;
+    transition: $transition;
+
+    &:hover {
+      background-color: $bg-tertiary;
+      border-color: $text-tertiary;
+    }
+
+    &:active {
+      transform: scale(0.98);
+    }
+  }
+
+  .search-filters {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    flex-wrap: wrap;
+  }
+
+  .search-container {
+    position: relative;
+    display: flex;
+    align-items: center;
+    min-width: 280px;
+
+    .search-icon {
+      position: absolute;
+      left: 12px;
+      color: $text-tertiary;
+      z-index: 1;
+    }
+
+    .search-input {
+      width: 100%;
+      padding: 10px 12px 10px 40px;
+      border: 1px solid $border-color;
+      border-radius: $border-radius;
+      font-size: 14px;
+      color: $text-primary;
+      background-color: $bg-primary;
+      transition: $transition;
+
+      &:focus {
+        outline: none;
+        border-color: $primary-color;
+        box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.1);
+      }
+
+      &::placeholder {
+        color: $text-tertiary;
+      }
+    }
+
+    .clear-search {
+      position: absolute;
+      right: 8px;
+      color: $text-tertiary;
+      padding: 4px;
+      transition: $transition;
+
+      &:hover {
+        color: $text-primary;
+      }
+    }
+  }
+
+  .filter-container {
+    min-width: 150px;
+  }
+
+  .filter-btn {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    padding: 10px 16px;
+    background-color: $secondary-color;
+    color: white;
+    border: none;
+    border-radius: $border-radius;
+    font-size: 14px;
+    font-weight: 500;
+    cursor: pointer;
+    transition: $transition;
+
+    &:hover {
+      background-color: #0f766e;
+      transform: translateY(-1px);
+      box-shadow: $shadow-md;
+    }
+
+    &:active {
+      transform: translateY(0);
+      box-shadow: $shadow-sm;
+    }
+  }
+}
+
+// 表格容器
+.table-container {
+  background-color: $bg-primary;
+  border-radius: $border-radius;
+  box-shadow: $shadow-sm;
+  overflow: hidden;
+  margin-bottom: 24px;
+}
+
+// 员工表格
+.employee-table {
+  width: 100%;
+
+  .mat-header-row {
+    background-color: $bg-tertiary;
+    font-weight: 600;
+    color: $text-primary;
+
+    .mat-header-cell {
+      padding: 16px;
+      font-size: 14px;
+      color: $text-primary;
+      font-weight: 600;
+      border-bottom: 1px solid $border-color;
+    }
+  }
+
+  .mat-row {
+    transition: $transition;
+    cursor: pointer;
+
+    &:hover {
+      background-color: color-mix(in srgb, $primary-color 2%, transparent);
+      transform: translateY(-1px);
+    }
+
+    .mat-cell {
+      padding: 16px;
+      font-size: 14px;
+      color: $text-secondary;
+      border-bottom: 1px solid $border-color;
+      vertical-align: middle;
+    }
+
+    &:last-child .mat-cell {
+      border-bottom: none;
+    }
+  }
+
+  .name-column {
+    min-width: 150px;
+
+    .employee-info {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+    }
+
+    .employee-avatar {
+      width: 36px;
+      height: 36px;
+      border-radius: 50%;
+      object-fit: cover;
+    }
+
+    .employee-name {
+      font-weight: 500;
+      color: $text-primary;
+    }
+  }
+
+  .actions-column {
+    min-width: 120px;
+
+    .action-buttons {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+    }
+
+    .edit-btn,
+    .delete-btn,
+    .view-btn {
+      color: $text-tertiary;
+      transition: $transition;
+      opacity: 0.8;
+
+      &:hover {
+        opacity: 1;
+        transform: scale(1.1);
+      }
+
+      &.edit-btn:hover {
+        color: $primary-color;
+      }
+
+      &.delete-btn:hover {
+        color: $error-color;
+      }
+
+      &.view-btn:hover {
+        color: $secondary-color;
+      }
+    }
+  }
+
+  // 合同信息样式
+  .contract-info {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+
+    .contract-date {
+      font-size: 12px;
+      color: $text-secondary;
+    }
+
+    .expiring-soon {
+      background-color: color-mix(in srgb, $warning-color 15%, transparent);
+      color: $warning-color;
+      padding: 2px 8px;
+      border-radius: 12px;
+      font-size: 11px;
+      font-weight: 500;
+      animation: pulse 2s infinite;
+    }
+
+    .contract-btn {
+      color: $primary-color;
+      transition: $transition;
+
+      &:hover {
+        color: $primary-light;
+        transform: scale(1.1);
+      }
+    }
+  }
+
+  .no-contract {
+    color: $text-tertiary;
+    font-size: 12px;
+    font-style: italic;
+  }
+
+  // 状态选择器样式
+  .status-select {
+    min-width: 100px;
+    .mat-select-trigger {
+      font-size: 14px;
+    }
+  }
+
+  // 空状态样式
+  .empty-state {
+    text-align: center;
+    padding: 60px 20px;
+
+    .empty-icon {
+      font-size: 48px;
+      color: $text-tertiary;
+      margin-bottom: 16px;
+    }
+
+    p {
+      color: $text-secondary;
+      font-size: 16px;
+      margin-bottom: 24px;
+    }
+  }
+}
+
+// 分页组件
+.pagination {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16px;
+  background-color: $bg-primary;
+  border-radius: $border-radius;
+  box-shadow: $shadow-sm;
+
+  .pagination-info {
+    font-size: 14px;
+    color: $text-secondary;
+  }
+
+  .mat-paginator {
+    display: flex;
+    justify-content: flex-end;
+    flex: 1;
+  }
+}
+
+// 自定义对话框样式
+.custom-dialog {
+  .mat-dialog-container {
+    border-radius: $border-radius;
+    box-shadow: $shadow-lg;
+    overflow: hidden;
+  }
+}
+
+// 自定义通知消息样式
+.success-snackbar {
+  background-color: $success-color;
+  color: white;
+}
+
+.error-snackbar {
+  background-color: $error-color;
+  color: white;
+}
+
+// 动画定义
+@keyframes slideIn {
+  from {
+    opacity: 0;
+    transform: translateY(-10px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+@keyframes pulse {
+  0%, 100% {
+    opacity: 1;
+  }
+  50% {
+    opacity: 0.7;
+  }
+}
+
+// 响应式设计
+@media (max-width: 1200px) {
+  .assets-container {
+    padding: 16px;
+  }
+
+  .action-bar {
+    flex-direction: column;
+    align-items: stretch;
+
+    .search-filters {
+      justify-content: space-between;
+    }
+  }
+}
+
+@media (max-width: 768px) {
+  .assets-container {
+    padding: 12px;
+  }
+
+  .page-header {
+    text-align: center;
+  }
+
+  .action-bar {
+    padding: 12px;
+
+    .search-filters {
+      flex-direction: column;
+      align-items: stretch;
+
+      .search-container,
+      .filter-container,
+      .filter-btn {
+        width: 100%;
+        min-width: auto;
+      }
+    }
+  }
+
+  .table-container {
+    overflow-x: auto;
+  }
+
+  .pagination {
+    flex-direction: column;
+    gap: 12px;
+    align-items: center;
+
+    .mat-paginator {
+      justify-content: center;
+    }
+  }
+}

+ 471 - 5
src/app/pages/hr/assets/assets.ts

@@ -1,11 +1,477 @@
-import { Component } from '@angular/core';
+import { Component, OnInit, signal, Inject } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
+import { MatButtonModule } from '@angular/material/button';
+import { MatInputModule } from '@angular/material/input';
+import { MatSelectModule } from '@angular/material/select';
+import { MatDatepickerModule } from '@angular/material/datepicker';
+import { MatNativeDateModule } from '@angular/material/core';
+import { MatDialogModule, MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { MatTableModule } from '@angular/material/table';
+import { MatPaginatorModule } from '@angular/material/paginator';
+import { MatCheckboxModule } from '@angular/material/checkbox';
+import { MatSnackBarModule, MatSnackBar } from '@angular/material/snack-bar';
+import { MatIconModule } from '@angular/material/icon';
+import { MatTooltipModule } from '@angular/material/tooltip';
+import { MatMenuModule } from '@angular/material/menu';
+import { Employee, Department, Position, Contract, Certificate, EmployeeStatus } from '../../../models/hr.model';
 
+// 创建新增员工对话框组件
+@Component({
+  selector: 'app-add-employee-dialog',
+  standalone: true,
+  imports: [
+    CommonModule,
+    ReactiveFormsModule,
+    MatInputModule,
+    MatSelectModule,
+    MatDatepickerModule,
+    MatNativeDateModule,
+    MatButtonModule
+  ],
+  template: `
+    <div class="dialog-header">
+      <h2>新增员工</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>
+    <form [formGroup]="employeeForm" (ngSubmit)="onSubmit()" class="employee-form">
+      <div class="form-row">
+        <div class="form-group">
+          <label>员工姓名 *</label>
+          <input matInput formControlName="name" placeholder="请输入姓名">
+        </div>
+        <div class="form-group">
+          <label>工号 *</label>
+          <input matInput formControlName="employeeId" placeholder="请输入工号">
+        </div>
+      </div>
+      <div class="form-row">
+        <div class="form-group">
+          <label>部门 *</label>
+          <mat-select formControlName="department">
+            <mat-option *ngFor="let dept of departments" [value]="dept.name">
+              {{ dept.name }}
+            </mat-option>
+          </mat-select>
+        </div>
+        <div class="form-group">
+          <label>岗位 *</label>
+          <input matInput formControlName="position" placeholder="请输入岗位">
+        </div>
+      </div>
+      <div class="form-row">
+        <div class="form-group">
+          <label>手机号码 *</label>
+          <input matInput formControlName="phone" placeholder="请输入手机号码">
+        </div>
+        <div class="form-group">
+          <label>邮箱 *</label>
+          <input matInput formControlName="email" placeholder="请输入邮箱">
+        </div>
+      </div>
+      <div class="form-row">
+        <div class="form-group">
+          <label>性别</label>
+          <mat-select formControlName="gender">
+            <mat-option value="男">男</mat-option>
+            <mat-option value="女">女</mat-option>
+          </mat-select>
+        </div>
+        <div class="form-group">
+          <label>出生日期</label>
+          <input matInput [matDatepicker]="birthDatePicker" formControlName="birthDate">
+          <mat-datepicker-toggle matSuffix [for]="birthDatePicker"></mat-datepicker-toggle>
+          <mat-datepicker #birthDatePicker></mat-datepicker>
+        </div>
+      </div>
+      <div class="form-row">
+        <div class="form-group">
+          <label>入职日期 *</label>
+          <input matInput [matDatepicker]="hireDatePicker" formControlName="hireDate">
+          <mat-datepicker-toggle matSuffix [for]="hireDatePicker"></mat-datepicker-toggle>
+          <mat-datepicker #hireDatePicker></mat-datepicker>
+        </div>
+        <div class="form-group">
+          <label>状态 *</label>
+          <mat-select formControlName="status">
+            <mat-option value="在职">在职</mat-option>
+            <mat-option value="试用期">试用期</mat-option>
+            <mat-option value="离职">离职</mat-option>
+          </mat-select>
+        </div>
+      </div>
+      <div class="dialog-actions">
+        <button type="button" mat-button (click)="dialogRef.close()">取消</button>
+        <button type="submit" mat-raised-button color="primary" [disabled]="!employeeForm.valid">保存</button>
+      </div>
+    </form>
+  `,
+  styles: [`
+    .dialog-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 24px;
+      padding-bottom: 16px;
+      border-bottom: 1px solid #e5e7eb;
+    }
+    .close-btn {
+      background: none;
+      border: none;
+      cursor: pointer;
+      color: #6b7280;
+      padding: 4px;
+    }
+    .employee-form {
+      max-width: 600px;
+    }
+    .form-row {
+      display: flex;
+      gap: 16px;
+      margin-bottom: 16px;
+    }
+    .form-group {
+      flex: 1;
+    }
+    label {
+      display: block;
+      margin-bottom: 4px;
+      font-weight: 500;
+      color: #374151;
+    }
+    .dialog-actions {
+      display: flex;
+      justify-content: flex-end;
+      gap: 12px;
+      margin-top: 24px;
+    }
+  `]
+}) class AddEmployeeDialog {
+  employeeForm: FormGroup;
+  departments = departmentsMock;
+  
+  constructor(
+    private fb: FormBuilder,
+    public dialogRef: MatDialogRef<AddEmployeeDialog>,
+    @Inject(MAT_DIALOG_DATA) public data: any
+  ) {
+    this.employeeForm = this.fb.group({
+      name: ['', Validators.required],
+      employeeId: ['', Validators.required],
+      department: ['', Validators.required],
+      position: ['', Validators.required],
+      phone: ['', [Validators.required, Validators.pattern(/^1[3-9]\d{9}$/)]],
+      email: ['', [Validators.required, Validators.email]],
+      gender: ['男'],
+      birthDate: [null],
+      hireDate: [new Date(), Validators.required],
+      status: ['在职', Validators.required]
+    });
+  }
+  
+  onSubmit() {
+    if (this.employeeForm.valid) {
+      this.dialogRef.close(this.employeeForm.value);
+    }
+  }
+}
+
+// 模拟数据
+const departmentsMock: Department[] = [
+  { id: '1', name: '管理层', employeeCount: 5 },
+  { id: '2', name: '设计部', employeeCount: 20 },
+  { id: '3', name: '客户服务部', employeeCount: 15 },
+  { id: '4', name: '财务部', employeeCount: 8 },
+  { id: '5', name: '人力资源部', employeeCount: 5 }
+];
+
+// 生成模拟员工数据
+const generateMockEmployees = (): Employee[] => {
+  const statuses: EmployeeStatus[] = ['在职', '试用期', '离职'];
+  const genders = ['男', '女'];
+  const departments = departmentsMock.map(d => d.name);
+  const positions = ['经理', '设计师', '客服专员', '财务专员', '人事专员', '助理'];
+  const employees: Employee[] = [];
+  
+  for (let i = 1; i <= 30; i++) {
+    const status = statuses[Math.floor(Math.random() * statuses.length)];
+    const hireDate = new Date();
+    hireDate.setMonth(hireDate.getMonth() - Math.floor(Math.random() * 36));
+    
+    // 随机生成合同信息
+    const contractEndDate = new Date(hireDate);
+    contractEndDate.setFullYear(contractEndDate.getFullYear() + 1);
+    
+    // 判断合同是否即将到期(7天内)
+    const today = new Date();
+    const daysUntilExpiry = Math.ceil((contractEndDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
+    const isExpiringSoon = daysUntilExpiry <= 7 && daysUntilExpiry >= 0;
+    
+    employees.push({
+      id: `emp-${i}`,
+      name: `员工${i}`,
+      employeeId: `EMP${2023}${String(i).padStart(3, '0')}`,
+      department: departments[Math.floor(Math.random() * departments.length)],
+      position: positions[Math.floor(Math.random() * positions.length)],
+      phone: `138${Math.floor(Math.random() * 100000000)}`,
+      email: `employee${i}@example.com`,
+      gender: genders[Math.floor(Math.random() * genders.length)],
+      birthDate: new Date(1980 + Math.floor(Math.random() * 20), Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1),
+      hireDate,
+      status,
+      avatar: `https://via.placeholder.com/40?text=${i}`,
+      contract: {
+        id: `contract-${i}`,
+        startDate: hireDate,
+        endDate: contractEndDate,
+        type: '劳动合同',
+        isExpiringSoon
+      },
+      certificates: i % 3 === 0 ? [
+        {
+          id: `cert-${i}-1`,
+          name: '身份证',
+          type: '身份证件',
+          number: `110101${Math.floor(Math.random() * 1000000000000000000)}`,
+          issueDate: new Date(2010 + Math.floor(Math.random() * 10), Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1),
+          expiryDate: new Date(2030 + Math.floor(Math.random() * 10), Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1)
+        }
+      ] : undefined
+    });
+  }
+  
+  return employees;
+};
+
+// 主组件
 @Component({
   selector: 'app-assets',
-  imports: [],
+  standalone: true,
+  imports: [
+    CommonModule,
+    FormsModule,
+    ReactiveFormsModule,
+    MatButtonModule,
+    MatInputModule,
+    MatSelectModule,
+    MatDialogModule,
+    MatTableModule,
+    MatPaginatorModule,
+    MatCheckboxModule,
+    MatSnackBarModule,
+    MatIconModule,
+    MatTooltipModule,
+    MatMenuModule
+  ],
   templateUrl: './assets.html',
   styleUrl: './assets.scss'
-})
-export class Assets {
-
+}) export class Assets implements OnInit {
+  // 暴露Math对象给模板使用
+  readonly Math = Math;
+  
+  // 员工数据
+  employees = signal<Employee[]>([]);
+  filteredEmployees = signal<Employee[]>([]);
+  selectedEmployees = signal<string[]>([]);
+  
+  // 搜索和筛选
+  searchTerm = signal('');
+  departmentFilter = signal('');
+  statusFilter = signal('');
+  
+  // 分页
+  pageSize = 10;
+  pageIndex = 0;
+  
+  // 表格列
+  displayedColumns: string[] = ['select', 'name', 'employeeId', 'department', 'position', 'phone', 'email', 'hireDate', 'status', 'contract', 'actions'];
+  
+  // 部门和状态选项
+  departments = departmentsMock;
+  statuses: EmployeeStatus[] = ['在职', '试用期', '离职'];
+  
+  constructor(
+    private dialog: MatDialog,
+    private snackBar: MatSnackBar
+  ) {}
+  
+  ngOnInit() {
+    // 加载模拟数据
+    this.employees.set(generateMockEmployees());
+    this.applyFilters();
+  }
+  
+  // 应用筛选
+  applyFilters() {
+    let filtered = this.employees();
+    
+    // 搜索筛选
+    if (this.searchTerm()) {
+      const term = this.searchTerm().toLowerCase();
+      filtered = filtered.filter(emp => 
+        emp.name.toLowerCase().includes(term) ||
+        emp.employeeId.toLowerCase().includes(term) ||
+        emp.phone.includes(term) ||
+        emp.email.toLowerCase().includes(term)
+      );
+    }
+    
+    // 部门筛选
+    if (this.departmentFilter()) {
+      filtered = filtered.filter(emp => emp.department === this.departmentFilter());
+    }
+    
+    // 状态筛选
+    if (this.statusFilter()) {
+      filtered = filtered.filter(emp => emp.status === this.statusFilter());
+    }
+    
+    this.filteredEmployees.set(filtered);
+  }
+  
+  // 打开新增员工对话框
+  openAddEmployeeDialog() {
+    const dialogRef = this.dialog.open(AddEmployeeDialog, {
+      width: '600px',
+      maxWidth: '90vw',
+      disableClose: true,
+      panelClass: 'custom-dialog'
+    });
+    
+    dialogRef.afterClosed().subscribe(result => {
+      if (result) {
+        const newEmployee: Employee = {
+          id: `emp-${Date.now()}`,
+          ...result,
+          avatar: `https://via.placeholder.com/40?text=E`,
+          contract: {
+            id: `contract-${Date.now()}`,
+            startDate: result.hireDate,
+            endDate: new Date(result.hireDate),
+            type: '劳动合同',
+            isExpiringSoon: false
+          }
+        };
+        
+        // 更新合同结束日期(假设一年合同)
+        if (newEmployee.contract) {
+          newEmployee.contract.endDate.setFullYear(newEmployee.contract.endDate.getFullYear() + 1);
+        }
+        
+        this.employees.update(emps => [newEmployee, ...emps]);
+        this.applyFilters();
+        this.showSuccessMessage('员工添加成功');
+      }
+    });
+  }
+  
+  // 编辑员工
+  editEmployee(employee: Employee) {
+    // 在实际应用中,这里会打开编辑对话框
+    this.showSuccessMessage('编辑功能待实现');
+  }
+  
+  // 删除员工
+  deleteEmployee(id: string) {
+    if (confirm('确定要删除该员工吗?')) {
+      this.employees.update(emps => emps.filter(emp => emp.id !== id));
+      this.applyFilters();
+      this.showSuccessMessage('员工删除成功');
+    }
+  }
+  
+  // 批量删除
+  batchDelete() {
+    if (this.selectedEmployees().length === 0) {
+      this.showErrorMessage('请先选择要删除的员工');
+      return;
+    }
+    
+    if (confirm(`确定要删除选中的 ${this.selectedEmployees().length} 名员工吗?`)) {
+      this.employees.update(emps => emps.filter(emp => !this.selectedEmployees().includes(emp.id)));
+      this.selectedEmployees.set([]);
+      this.applyFilters();
+      this.showSuccessMessage('批量删除成功');
+    }
+  }
+  
+  // 导出数据
+  exportData(format: 'excel' | 'pdf') {
+    this.showSuccessMessage(`${format.toUpperCase()} 导出功能待实现`);
+  }
+  
+  // 切换员工选择
+  toggleEmployeeSelection(id: string) {
+    this.selectedEmployees.update(selected => {
+      if (selected.includes(id)) {
+        return selected.filter(selectedId => selectedId !== id);
+      } else {
+        return [...selected, id];
+      }
+    });
+  }
+  
+  // 全选/取消全选
+  toggleSelectAll() {
+    if (this.isAllSelected()) {
+      this.selectedEmployees.set([]);
+    } else {
+      this.selectedEmployees.set(this.filteredEmployees().map(emp => emp.id));
+    }
+  }
+  
+  // 判断是否全选
+  isAllSelected() {
+    return this.selectedEmployees().length > 0 && 
+           this.selectedEmployees().length === this.filteredEmployees().length;
+  }
+  
+  // 切换员工状态
+  changeEmployeeStatus(employee: Employee, newStatus: EmployeeStatus) {
+    if (employee.status === newStatus) return;
+    
+    this.employees.update(emps => 
+      emps.map(emp => 
+        emp.id === employee.id ? { ...emp, status: newStatus } : emp
+      )
+    );
+    this.applyFilters();
+    this.showSuccessMessage(`员工状态已更新为${newStatus}`);
+  }
+  
+  // 格式化日期
+  formatDate(date: Date | string): 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')}`;
+  }
+  
+  // 显示成功消息
+  showSuccessMessage(message: string) {
+    this.snackBar.open(message, '关闭', {
+      duration: 3000,
+      verticalPosition: 'top',
+      panelClass: 'success-snackbar'
+    });
+  }
+  
+  // 显示错误消息
+  showErrorMessage(message: string) {
+    this.snackBar.open(message, '关闭', {
+      duration: 3000,
+      verticalPosition: 'top',
+      panelClass: 'error-snackbar'
+    });
+  }
+  
+  // 分页事件处理
+  onPageChange(event: any) {
+    this.pageIndex = event.pageIndex;
+    this.pageSize = event.pageSize;
+  }
 }

+ 236 - 1
src/app/pages/hr/attendance/attendance.html

@@ -1 +1,236 @@
-<p>attendance works!</p>
+<div class="attendance-container">
+  <header class="page-header">
+    <h1>考勤统计</h1>
+    <p class="page-description">管理和统计员工考勤数据,支持多维度查看和分析</p>
+  </header>
+
+  <!-- 时间维度切换栏 -->
+  <div class="time-dimension-bar">
+    <div class="date-navigation">
+      <button mat-icon-button (click)="navigateDate('prev')" class="nav-btn">
+        <mat-icon>chevron_left</mat-icon>
+      </button>
+      <div class="current-date" (click)="selectedView.set('month')">
+        {{ selectedDate().getFullYear() }}年{{ selectedDate().getMonth() + 1 }}月
+        <mat-icon class="calendar-icon">calendar_today</mat-icon>
+      </div>
+      <button mat-icon-button (click)="navigateDate('next')" class="nav-btn">
+        <mat-icon>chevron_right</mat-icon>
+      </button>
+    </div>
+    
+    <div class="view-tabs">
+      <button 
+        mat-raised-button 
+        [class.active]="selectedView() === 'day'"
+        (click)="switchView('day')"
+        class="view-btn"
+      >
+        日
+      </button>
+      <button 
+        mat-raised-button 
+        [class.active]="selectedView() === 'week'"
+        (click)="switchView('week')"
+        class="view-btn"
+      >
+        周
+      </button>
+      <button 
+        mat-raised-button 
+        [class.active]="selectedView() === 'month'"
+        (click)="switchView('month')"
+        class="view-btn"
+      >
+        月
+      </button>
+    </div>
+    
+    <div class="action-buttons">
+      <button mat-raised-button color="primary" (click)="exportAttendanceData()" class="export-btn">
+        <mat-icon>file_download</mat-icon>
+        导出考勤数据
+      </button>
+    </div>
+  </div>
+
+  <!-- 主内容区 -->
+  <div class="main-content">
+    <!-- 左侧:考勤日历 -->
+    <div class="calendar-section">
+      <div class="section-header">
+        <h2>考勤日历</h2>
+      </div>
+      
+      <!-- 日历表头 -->
+      <div class="calendar-header">
+        <div class="weekday" *ngFor="let i of [0, 1, 2, 3, 4, 5, 6]">
+          {{ getWeekdayName(i) }}
+        </div>
+      </div>
+      
+      <!-- 日历格子 -->
+      <div class="calendar-grid">
+        <div 
+          *ngFor="let day of getCalendarDays()"
+          class="calendar-day"
+          [class.current-month]="day.currentMonth"
+          [class.other-month]="!day.currentMonth"
+          [class.today]="isToday(day.date)"
+          [class.has-attendance]="day.attendance"
+          [class.absent]="day.attendance && day.attendance.status === '旷工'"
+          [class.late]="day.attendance && day.attendance.status === '迟到'"
+          [class.early]="day.attendance && day.attendance.status === '早退'"
+          [class.leave]="day.attendance && day.attendance.status === '请假'"
+          [class.normal]="day.attendance && day.attendance.status === '正常'"
+          (click)="selectedDate.set(day.date)"
+          matTooltip="{{ getDayTooltip(day) }}"
+          matTooltipPosition="above"
+        >
+          <span class="day-number">{{ day.dayOfMonth }}</span>
+          <div *ngIf="day.attendance" class="attendance-indicator"></div>
+        </div>
+      </div>
+      
+      <!-- 图例 -->
+      <div class="calendar-legend">
+        <div class="legend-item">
+          <div class="legend-dot normal"></div>
+          <span>正常</span>
+        </div>
+        <div class="legend-item">
+          <div class="legend-dot late"></div>
+          <span>迟到</span>
+        </div>
+        <div class="legend-item">
+          <div class="legend-dot early"></div>
+          <span>早退</span>
+        </div>
+        <div class="legend-item">
+          <div class="legend-dot absent"></div>
+          <span>旷工</span>
+        </div>
+        <div class="legend-item">
+          <div class="legend-dot leave"></div>
+          <span>请假</span>
+        </div>
+      </div>
+    </div>
+    
+    <!-- 右侧:统计图表和异常列表 -->
+    <div class="stats-section">
+      <!-- 统计卡片 -->
+      <div class="stats-cards">
+        <div class="stat-card">
+          <div class="stat-value">{{ attendanceStats().normalDays }}/{{ attendanceStats().totalDays }}</div>
+          <div class="stat-label">正常出勤</div>
+          <div class="stat-rate">{{ attendanceStats().complianceRate }}%</div>
+        </div>
+        <div class="stat-card warning">
+          <div class="stat-value">{{ attendanceStats().lateDays }}</div>
+          <div class="stat-label">迟到</div>
+        </div>
+        <div class="stat-card warning">
+          <div class="stat-value">{{ attendanceStats().earlyLeaveDays }}</div>
+          <div class="stat-label">早退</div>
+        </div>
+        <div class="stat-card danger">
+          <div class="stat-value">{{ attendanceStats().absentDays }}</div>
+          <div class="stat-label">旷工</div>
+        </div>
+        <div class="stat-card info">
+          <div class="stat-value">{{ attendanceStats().leaveDays }}</div>
+          <div class="stat-label">请假</div>
+        </div>
+        <div class="stat-card primary">
+          <div class="stat-value">{{ attendanceStats().totalWorkHours }}h</div>
+          <div class="stat-label">总工时</div>
+          <div class="stat-sub">{{ attendanceStats().avgWorkHours }}h/天</div>
+        </div>
+      </div>
+      
+      <!-- 部门考勤对比 -->
+      <div class="department-comparison">
+        <div class="section-header">
+          <h2>部门考勤对比</h2>
+        </div>
+        <div class="department-chart">
+          <div *ngFor="let dept of departmentAttendanceData()" class="dept-bar-container">
+            <div class="dept-info">
+              <span class="dept-name">{{ dept.department }}</span>
+              <span class="dept-rate">{{ dept.complianceRate }}%</span>
+            </div>
+            <div class="progress-bar">
+              <div 
+                class="progress-fill"
+                [style.width]="dept.complianceRate + '%'"
+              ></div>
+            </div>
+            <div class="dept-stats">
+              正常 {{ dept.compliant }} / 总计 {{ dept.total }}
+            </div>
+          </div>
+        </div>
+      </div>
+      
+      <!-- 异常考勤列表 -->
+      <div class="exceptions-section">
+        <div class="section-header">
+          <h2>考勤异常</h2>
+          <span class="exception-count">({{ exceptionAttendance().length }})</span>
+        </div>
+        <div class="exceptions-table">
+          <table mat-table [dataSource]="exceptionAttendance()" class="exception-table">
+            <ng-container matColumnDef="date">
+              <th mat-header-cell *matHeaderCellDef>日期</th>
+              <td mat-cell *matCellDef="let item">{{ formatDate(item.date) }}</td>
+            </ng-container>
+            <ng-container matColumnDef="employeeName">
+              <th mat-header-cell *matHeaderCellDef>员工</th>
+              <td mat-cell *matCellDef="let item">{{ getEmployeeName(item.employeeId) }}</td>
+            </ng-container>
+            <ng-container matColumnDef="status">
+              <th mat-header-cell *matHeaderCellDef>异常类型</th>
+              <td mat-cell *matCellDef="let item">
+                <span class="status-badge" [class]="getStatusClass(item.status)">
+                  {{ item.status }}
+                </span>
+              </td>
+            </ng-container>
+            <ng-container matColumnDef="workHours">
+              <th mat-header-cell *matHeaderCellDef>工时</th>
+              <td mat-cell *matCellDef="let item">{{ item.workHours }}h</td>
+            </ng-container>
+            <ng-container matColumnDef="projectName">
+              <th mat-header-cell *matHeaderCellDef>项目</th>
+              <td mat-cell *matCellDef="let item">{{ item.projectName }}</td>
+            </ng-container>
+            <ng-container matColumnDef="actions">
+              <th mat-header-cell *matHeaderCellDef>操作</th>
+              <td mat-cell *matCellDef="let item">
+                <button 
+                  mat-raised-button 
+                  color="primary"
+                  size="small"
+                  (click)="openAttendanceDialog(item)"
+                  class="fix-btn"
+                >
+                  补卡申请
+                </button>
+              </td>
+            </ng-container>
+            
+            <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
+            <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
+            
+            <tr class="mat-row" *matNoDataRow>
+              <td class="mat-cell no-data" [attr.colspan]="displayedColumns.length">
+                暂无考勤异常记录
+              </td>
+            </tr>
+          </table>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>

+ 711 - 0
src/app/pages/hr/attendance/attendance.scss

@@ -0,0 +1,711 @@
+// 自定义主题
+$primary-color: #1e40af; // 深蓝主色,传递可靠感
+$primary-light: #3b82f6; // 浅蓝色,用于悬停效果
+$secondary-color: #0d9488; // 薄荷绿,作为强调色
+$success-color: #10b981; // 成功色
+$warning-color: #f59e0b; // 警告色(浅橙)
+$error-color: #ef4444; // 错误色
+$info-color: #3b82f6; // 信息色
+$text-primary: #1f2937; // 主要文本色
+$text-secondary: #4b5563; // 次要文本色
+$text-tertiary: #9ca3af; // 辅助文本色
+$bg-primary: #ffffff; // 主背景色
+$bg-secondary: #f9fafb; // 次要背景色
+$bg-tertiary: #f3f4f6; // 辅助背景色
+$border-color: #e5e7eb; // 边框色
+$shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+$shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+$shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+$border-radius: 8px;
+$transition: all 0.2s ease;
+
+// 主容器样式
+.attendance-container {
+  padding: 24px;
+  min-height: 100vh;
+  background-color: $bg-secondary;
+  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+}
+
+// 页面标题
+.page-header {
+  margin-bottom: 24px;
+  padding-bottom: 16px;
+  border-bottom: 2px solid $border-color;
+
+  h1 {
+    font-size: 28px;
+    font-weight: 700;
+    color: $text-primary;
+    margin: 0 0 8px 0;
+  }
+
+  .page-description {
+    font-size: 16px;
+    color: $text-secondary;
+    margin: 0;
+  }
+}
+
+// 时间维度切换栏
+.time-dimension-bar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 24px;
+  padding: 16px 20px;
+  background-color: $bg-primary;
+  border-radius: $border-radius;
+  box-shadow: $shadow-sm;
+  flex-wrap: wrap;
+  gap: 16px;
+
+  .date-navigation {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+  }
+
+  .nav-btn {
+    color: $text-secondary;
+    transition: $transition;
+
+    &:hover {
+      color: $primary-color;
+      background-color: $bg-tertiary;
+    }
+  }
+
+  .current-date {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    font-size: 18px;
+    font-weight: 600;
+    color: $text-primary;
+    cursor: pointer;
+    padding: 8px 16px;
+    border-radius: $border-radius;
+    transition: $transition;
+
+    &:hover {
+      background-color: $bg-tertiary;
+    }
+
+    .calendar-icon {
+      font-size: 16px;
+    }
+  }
+
+  .view-tabs {
+    display: flex;
+    align-items: center;
+    background-color: $bg-tertiary;
+    border-radius: $border-radius;
+    padding: 4px;
+  }
+
+  .view-btn {
+    padding: 8px 20px;
+    border-radius: $border-radius;
+    font-size: 14px;
+    font-weight: 500;
+    transition: $transition;
+
+    &.active {
+      background-color: $primary-color;
+      color: white;
+    }
+
+    &:not(.active) {
+      background-color: transparent;
+      color: $text-secondary;
+
+      &:hover {
+        background-color: $bg-secondary;
+        color: $text-primary;
+      }
+    }
+  }
+
+  .action-buttons {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+  }
+
+  .export-btn {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    padding: 10px 20px;
+    background-color: $primary-color;
+    color: white;
+    border: none;
+    border-radius: $border-radius;
+    font-size: 14px;
+    font-weight: 500;
+    cursor: pointer;
+    transition: $transition;
+
+    &:hover {
+      background-color: $primary-light;
+      transform: translateY(-1px);
+      box-shadow: $shadow-md;
+    }
+
+    &:active {
+      transform: translateY(0);
+      box-shadow: $shadow-sm;
+    }
+  }
+}
+
+// 主内容区
+.main-content {
+  display: grid;
+  grid-template-columns: 1fr;
+  gap: 24px;
+  align-items: start;
+}
+
+// 左侧:考勤日历
+.calendar-section {
+  background-color: $bg-primary;
+  border-radius: $border-radius;
+  box-shadow: $shadow-sm;
+  padding: 20px;
+  height: fit-content;
+  position: sticky;
+  top: 24px;
+
+  .section-header {
+    margin-bottom: 20px;
+
+    h2 {
+      font-size: 20px;
+      font-weight: 600;
+      color: $text-primary;
+      margin: 0;
+    }
+  }
+
+  .calendar-header {
+    display: grid;
+    grid-template-columns: repeat(7, 1fr);
+    gap: 4px;
+    margin-bottom: 8px;
+
+    .weekday {
+      text-align: center;
+      font-size: 14px;
+      font-weight: 600;
+      color: $text-secondary;
+      padding: 8px;
+    }
+  }
+
+  .calendar-grid {
+    display: grid;
+    grid-template-columns: repeat(7, 1fr);
+    gap: 4px;
+    margin-bottom: 16px;
+  }
+
+  .calendar-day {
+    aspect-ratio: 1;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    border-radius: $border-radius;
+    cursor: pointer;
+    position: relative;
+    transition: $transition;
+    background-color: $bg-secondary;
+
+    &.current-month {
+      background-color: $bg-secondary;
+    }
+
+    &.other-month {
+      opacity: 0.4;
+    }
+
+    &.today {
+      border: 2px solid $primary-color;
+      font-weight: 600;
+    }
+
+    &:hover {
+      transform: scale(1.05);
+      box-shadow: $shadow-md;
+    }
+
+    .day-number {
+      font-size: 14px;
+      color: $text-primary;
+    }
+
+    .attendance-indicator {
+      width: 8px;
+      height: 8px;
+      border-radius: 50%;
+      margin-top: 4px;
+      transition: $transition;
+    }
+
+    &.normal .attendance-indicator {
+      background-color: $success-color;
+    }
+
+    &.late .attendance-indicator {
+      background-color: $warning-color;
+    }
+
+    &.early .attendance-indicator {
+      background-color: $warning-color;
+    }
+
+    &.absent .attendance-indicator {
+      background-color: $error-color;
+    }
+
+    &.leave .attendance-indicator {
+      background-color: $info-color;
+    }
+
+    &.normal {
+      background-color: color-mix(in srgb, $success-color 5%, $bg-secondary);
+    }
+
+    &.late {
+      background-color: color-mix(in srgb, $warning-color 5%, $bg-secondary);
+    }
+
+    &.early {
+      background-color: color-mix(in srgb, $warning-color 5%, $bg-secondary);
+    }
+
+    &.absent {
+      background-color: color-mix(in srgb, $error-color 5%, $bg-secondary);
+    }
+
+    &.leave {
+      background-color: color-mix(in srgb, $info-color 5%, $bg-secondary);
+    }
+  }
+
+  .calendar-legend {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 12px;
+    margin-top: 16px;
+    padding-top: 16px;
+    border-top: 1px solid $border-color;
+
+    .legend-item {
+      display: flex;
+      align-items: center;
+      gap: 6px;
+      font-size: 12px;
+      color: $text-secondary;
+
+      .legend-dot {
+        width: 8px;
+        height: 8px;
+        border-radius: 50%;
+      }
+
+      .legend-dot.normal {
+        background-color: $success-color;
+      }
+
+      .legend-dot.late {
+        background-color: $warning-color;
+      }
+
+      .legend-dot.early {
+        background-color: $warning-color;
+      }
+
+      .legend-dot.absent {
+        background-color: $error-color;
+      }
+
+      .legend-dot.leave {
+        background-color: $info-color;
+      }
+    }
+  }
+}
+
+// 右侧:统计图表和异常列表
+.stats-section {
+  display: flex;
+  flex-direction: column;
+  gap: 24px;
+}
+
+// 统计卡片
+.stats-cards {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
+  gap: 16px;
+  margin-bottom: 24px;
+
+  .stat-card {
+    background-color: $bg-primary;
+    border-radius: $border-radius;
+    box-shadow: $shadow-sm;
+    padding: 20px;
+    text-align: center;
+    transition: $transition;
+    position: relative;
+    overflow: hidden;
+
+    &:hover {
+      transform: translateY(-2px);
+      box-shadow: $shadow-md;
+    }
+
+    .stat-value {
+      font-size: 28px;
+      font-weight: 700;
+      color: $text-primary;
+      margin-bottom: 4px;
+    }
+
+    .stat-label {
+      font-size: 14px;
+      color: $text-secondary;
+      margin-bottom: 8px;
+    }
+
+    .stat-rate {
+      font-size: 16px;
+      font-weight: 600;
+      color: $success-color;
+    }
+
+    .stat-sub {
+      font-size: 12px;
+      color: $text-tertiary;
+    }
+
+    &.primary {
+      border-top: 3px solid $primary-color;
+    }
+
+    &.warning {
+      border-top: 3px solid $warning-color;
+    }
+
+    &.danger {
+      border-top: 3px solid $error-color;
+    }
+
+    &.info {
+      border-top: 3px solid $info-color;
+    }
+  }
+}
+
+// 部门考勤对比
+.department-comparison {
+  background-color: $bg-primary;
+  border-radius: $border-radius;
+  box-shadow: $shadow-sm;
+  padding: 20px;
+  margin-bottom: 24px;
+
+  .section-header {
+    margin-bottom: 20px;
+
+    h2 {
+      font-size: 20px;
+      font-weight: 600;
+      color: $text-primary;
+      margin: 0;
+    }
+  }
+
+  .department-chart {
+    display: flex;
+    flex-direction: column;
+    gap: 16px;
+  }
+
+  .dept-bar-container {
+    display: flex;
+    align-items: center;
+    gap: 16px;
+    transition: $transition;
+
+    &:hover {
+      transform: translateX(4px);
+    }
+
+    .dept-info {
+      display: flex;
+      flex-direction: column;
+      min-width: 120px;
+
+      .dept-name {
+        font-size: 14px;
+        font-weight: 500;
+        color: $text-primary;
+      }
+
+      .dept-rate {
+        font-size: 18px;
+        font-weight: 700;
+        color: $primary-color;
+      }
+    }
+
+    .progress-bar {
+      flex: 1;
+      height: 12px;
+      background-color: $bg-tertiary;
+      border-radius: 6px;
+      overflow: hidden;
+      position: relative;
+    }
+
+    .progress-fill {
+      height: 100%;
+      background-color: $primary-color;
+      border-radius: 6px;
+      transition: width 1s ease-in-out;
+      position: relative;
+      overflow: hidden;
+
+      &::after {
+        content: '';
+        position: absolute;
+        top: 0;
+        left: 0;
+        right: 0;
+        bottom: 0;
+        background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
+        animation: progressAnimation 1.5s infinite;
+      }
+    }
+
+    .dept-stats {
+      font-size: 12px;
+      color: $text-tertiary;
+      min-width: 100px;
+      text-align: right;
+    }
+  }
+}
+
+// 异常考勤列表
+.exceptions-section {
+  background-color: $bg-primary;
+  border-radius: $border-radius;
+  box-shadow: $shadow-sm;
+  padding: 20px;
+
+  .section-header {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    margin-bottom: 20px;
+
+    h2 {
+      font-size: 20px;
+      font-weight: 600;
+      color: $text-primary;
+      margin: 0;
+    }
+
+    .exception-count {
+      font-size: 14px;
+      color: $text-tertiary;
+      background-color: $bg-tertiary;
+      padding: 2px 8px;
+      border-radius: 12px;
+    }
+  }
+
+  .exceptions-table {
+    overflow-x: auto;
+  }
+
+  .exception-table {
+    width: 100%;
+    min-width: 600px;
+
+    .mat-header-row {
+      background-color: $bg-tertiary;
+      font-weight: 600;
+      color: $text-primary;
+
+      .mat-header-cell {
+        padding: 12px;
+        font-size: 14px;
+        color: $text-primary;
+        font-weight: 600;
+        border-bottom: 1px solid $border-color;
+      }
+    }
+
+    .mat-row {
+      transition: $transition;
+      cursor: pointer;
+
+      &:hover {
+        background-color: color-mix(in srgb, $primary-color 2%, transparent);
+      }
+
+      .mat-cell {
+        padding: 12px;
+        font-size: 14px;
+        color: $text-secondary;
+        border-bottom: 1px solid $border-color;
+        vertical-align: middle;
+      }
+
+      &:last-child .mat-cell {
+        border-bottom: none;
+      }
+    }
+
+    .status-badge {
+      padding: 2px 8px;
+      border-radius: 12px;
+      font-size: 12px;
+      font-weight: 500;
+      display: inline-block;
+    }
+
+    .status-normal {
+      background-color: color-mix(in srgb, $success-color 15%, transparent);
+      color: $success-color;
+    }
+
+    .status-late {
+      background-color: color-mix(in srgb, $warning-color 15%, transparent);
+      color: $warning-color;
+    }
+
+    .status-early {
+      background-color: color-mix(in srgb, $warning-color 15%, transparent);
+      color: $warning-color;
+    }
+
+    .status-absent {
+      background-color: color-mix(in srgb, $error-color 15%, transparent);
+      color: $error-color;
+    }
+
+    .status-leave {
+      background-color: color-mix(in srgb, $info-color 15%, transparent);
+      color: $info-color;
+    }
+
+    .fix-btn {
+      padding: 6px 12px;
+      font-size: 12px;
+      transition: $transition;
+
+      &:hover {
+        transform: scale(1.05);
+      }
+    }
+
+    .no-data {
+      text-align: center;
+      padding: 40px 20px;
+      color: $text-tertiary;
+      font-style: italic;
+    }
+  }
+}
+
+// 动画定义
+@keyframes progressAnimation {
+  0% {
+    transform: translateX(-100%);
+  }
+  100% {
+    transform: translateX(100%);
+  }
+}
+
+// 响应式设计
+@media (max-width: 1200px) {
+  .attendance-container {
+    padding: 16px;
+  }
+
+  .main-content {
+    grid-template-columns: 1fr;
+  }
+
+  .calendar-section {
+    position: relative;
+    top: 0;
+  }
+}
+
+@media (max-width: 768px) {
+  .attendance-container {
+    padding: 12px;
+  }
+
+  .page-header {
+    text-align: center;
+  }
+
+  .time-dimension-bar {
+    flex-direction: column;
+    align-items: stretch;
+    gap: 12px;
+
+    .date-navigation,
+    .view-tabs,
+    .action-buttons {
+      justify-content: center;
+    }
+  }
+
+  .stats-cards {
+    grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
+    gap: 12px;
+  }
+
+  .stat-card {
+    padding: 16px;
+
+    .stat-value {
+      font-size: 24px;
+    }
+
+    .stat-label {
+      font-size: 12px;
+    }
+  }
+
+  .department-comparison {
+    .dept-bar-container {
+      flex-direction: column;
+      align-items: flex-start;
+      gap: 8px;
+    }
+
+    .dept-info {
+      flex-direction: row;
+      gap: 16px;
+      min-width: auto;
+    }
+
+    .dept-stats {
+      text-align: left;
+      min-width: auto;
+    }
+  }
+}

+ 562 - 5
src/app/pages/hr/attendance/attendance.ts

@@ -1,11 +1,568 @@
-import { Component } from '@angular/core';
+import { Component, OnInit, signal, computed, Inject } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { MatButtonModule } from '@angular/material/button';
+import { MatTabsModule } from '@angular/material/tabs';
+import { MatIconModule } from '@angular/material/icon';
+import { MatTableModule } from '@angular/material/table';
+import { MatCardModule } from '@angular/material/card';
+import { MatTooltipModule } from '@angular/material/tooltip';
+import { MatDialogModule, MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { Attendance as AttendanceModel, Employee } from '../../../models/hr.model';
 
+// 补卡申请对话框组件
+@Component({
+  selector: 'app-attendance-dialog',
+  standalone: true,
+  imports: [
+    CommonModule,
+    FormsModule,
+    MatButtonModule,
+    MatIconModule
+  ],
+  template: `
+    <div class="dialog-header">
+      <h2>补卡申请</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="info-item">
+        <label>员工姓名:</label>
+        <span>{{ attendanceData.employeeName }}</span>
+      </div>
+      <div class="info-item">
+        <label>异常日期:</label>
+        <span>{{ formatDate(attendanceData.date) }}</span>
+      </div>
+      <div class="info-item">
+        <label>异常类型:</label>
+        <span>{{ attendanceData.status }}</span>
+      </div>
+      <div class="form-group">
+        <label>补卡说明:</label>
+        <textarea 
+          [(ngModel)]="reason" 
+          placeholder="请输入补卡原因..." 
+          rows="3"
+          class="reason-input"
+        ></textarea>
+      </div>
+    </div>
+    <div class="dialog-actions">
+      <button mat-button (click)="dialogRef.close()">取消</button>
+      <button mat-raised-button color="primary" (click)="submit()">提交申请</button>
+    </div>
+  `,
+  styles: [`
+    .dialog-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 24px;
+      padding-bottom: 16px;
+      border-bottom: 1px solid #e5e7eb;
+    }
+    .close-btn {
+      background: none;
+      border: none;
+      cursor: pointer;
+      color: #6b7280;
+      padding: 4px;
+    }
+    .dialog-content {
+      max-width: 500px;
+    }
+    .info-item {
+      display: flex;
+      justify-content: space-between;
+      margin-bottom: 12px;
+      padding: 8px 0;
+      border-bottom: 1px solid #f3f4f6;
+    }
+    .form-group {
+      margin-top: 20px;
+    }
+    label {
+      display: block;
+      margin-bottom: 4px;
+      font-weight: 500;
+      color: #374151;
+    }
+    .reason-input {
+      width: 100%;
+      padding: 8px 12px;
+      border: 1px solid #e5e7eb;
+      border-radius: 6px;
+      font-size: 14px;
+      resize: vertical;
+    }
+    .dialog-actions {
+      display: flex;
+      justify-content: flex-end;
+      gap: 12px;
+      margin-top: 24px;
+    }
+  `]
+}) class AttendanceDialog {
+  attendanceData: any;
+  reason = '';
+  
+  constructor(
+    public dialogRef: MatDialogRef<AttendanceDialog>,
+    @Inject(MAT_DIALOG_DATA) public data: any
+  ) {
+    this.attendanceData = data;
+  }
+  
+  formatDate(date: Date | string): 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')}`;
+  }
+  
+  submit() {
+    if (!this.reason.trim()) {
+      alert('请输入补卡原因');
+      return;
+    }
+    this.dialogRef.close({ reason: this.reason });
+  }
+}
+
+// 生成模拟考勤数据
+const generateMockAttendanceData = (): AttendanceModel[] => {
+  const statuses: Array<'正常' | '迟到' | '早退' | '旷工' | '请假'> = ['正常', '迟到', '早退', '旷工', '请假'];
+  const attendanceList: AttendanceModel[] = [];
+  const today = new Date();
+  const projectIds = ['proj-001', 'proj-002', 'proj-003', 'proj-004'];
+  const projectNames = ['现代风格客厅设计', '欧式厨房改造', '极简卧室设计', '办公室规划'];
+  
+  // 生成最近30天的考勤数据
+  for (let i = 29; i >= 0; i--) {
+    const date = new Date();
+    date.setDate(today.getDate() - i);
+    
+    // 为每个部门生成3-5个员工的考勤数据
+    const departmentCount = Math.floor(Math.random() * 3) + 3;
+    
+    for (let j = 1; j <= departmentCount; j++) {
+      const statusIndex = Math.floor(Math.random() * 10);
+      let status: typeof statuses[0];
+      
+      // 80%概率正常,10%概率迟到,5%概率早退,3%概率请假,2%概率旷工
+      if (statusIndex < 8) {
+        status = '正常';
+      } else if (statusIndex < 9) {
+        status = '迟到';
+      } else if (statusIndex < 9.5) {
+        status = '早退';
+      } else if (statusIndex < 9.8) {
+        status = '请假';
+      } else {
+        status = '旷工';
+      }
+      
+      // 随机选择一个项目
+      const projectIndex = Math.floor(Math.random() * projectIds.length);
+      const projectId = projectIds[projectIndex];
+      const projectName = projectNames[projectIndex];
+      
+      // 生成打卡时间
+      let checkInTime: Date | undefined;
+      let checkOutTime: Date | undefined;
+      let workHours = 0;
+      
+      if (status !== '旷工' && status !== '请假') {
+        checkInTime = new Date(date);
+        checkInTime.setHours(9 + (status === '迟到' ? Math.floor(Math.random() * 2) : 0));
+        checkInTime.setMinutes(Math.floor(Math.random() * 60));
+        
+        checkOutTime = new Date(date);
+        checkOutTime.setHours(18 - (status === '早退' ? Math.floor(Math.random() * 2) : 0));
+        checkOutTime.setMinutes(Math.floor(Math.random() * 60));
+        
+        // 计算工作时长(小时)
+        workHours = Math.round((checkOutTime.getTime() - checkInTime.getTime()) / (1000 * 60 * 60) * 10) / 10;
+      }
+      
+      attendanceList.push({
+        id: `att-${date.getTime()}-${j}`,
+        employeeId: `emp-${j}`,
+        date,
+        checkInTime,
+        checkOutTime,
+        status,
+        workHours,
+        projectId,
+        projectName
+      });
+    }
+  }
+  
+  return attendanceList;
+};
+
+// 生成模拟员工数据
+const generateMockEmployees = (): Employee[] => {
+  const employees: Employee[] = [];
+  const departments = ['设计部', '客户服务部', '财务部', '人力资源部'];
+  const positions = ['设计师', '客服专员', '财务专员', '人事专员', '经理', '助理'];
+  const names = ['张三', '李四', '王五', '赵六', '钱七', '孙八', '周九', '吴十'];
+  
+  for (let i = 1; i <= 20; i++) {
+    employees.push({
+      id: `emp-${i}`,
+      name: names[i % names.length] + i,
+      department: departments[Math.floor(Math.random() * departments.length)],
+      position: positions[Math.floor(Math.random() * positions.length)],
+      employeeId: `EMP2023${String(i).padStart(3, '0')}`,
+      phone: `138${Math.floor(Math.random() * 100000000)}`,
+      email: `employee${i}@example.com`,
+      gender: i % 2 === 0 ? '女' : '男',
+      birthDate: new Date(1980 + Math.floor(Math.random() * 20), Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1),
+      hireDate: new Date(2020 + Math.floor(Math.random() * 3), Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1),
+      status: '在职',
+      avatar: `https://via.placeholder.com/40?text=${i}`
+    });
+  }
+  
+  return employees;
+};
+
+// 主组件
 @Component({
   selector: 'app-attendance',
-  imports: [],
+  standalone: true,
+  imports: [
+    CommonModule,
+    FormsModule,
+    MatButtonModule,
+    MatTabsModule,
+    MatIconModule,
+    MatTableModule,
+    MatCardModule,
+    MatTooltipModule,
+    MatDialogModule
+  ],
   templateUrl: './attendance.html',
   styleUrl: './attendance.scss'
-})
-export class Attendance {
-
+}) export class Attendance implements OnInit {
+  // 数据
+  attendanceData = signal<AttendanceModel[]>([]);
+  employees = signal<Employee[]>([]);
+  selectedView = signal<'day' | 'week' | 'month'>('month');
+  selectedDate = signal<Date>(new Date());
+  selectedEmployeeId = signal<string>('');
+  selectedProjectId = signal<string>('');
+  
+  // 获取日期tooltip信息
+  getDayTooltip(day: any): string {
+    if (!day.attendance) {
+      return '休息日';
+    }
+    
+    const status = day.attendance.status;
+    let tooltip = `状态: ${status}`;
+    
+    if (day.attendance.workHours) {
+      tooltip += `\n工作时长: ${day.attendance.workHours}小时`;
+    }
+    
+    if (day.attendance.checkInTime) {
+      const checkIn = new Date(day.attendance.checkInTime);
+      tooltip += `\n签到: ${checkIn.getHours().toString().padStart(2, '0')}:${checkIn.getMinutes().toString().padStart(2, '0')}`;
+    }
+    
+    if (day.attendance.checkOutTime) {
+      const checkOut = new Date(day.attendance.checkOutTime);
+      tooltip += `\n签退: ${checkOut.getHours().toString().padStart(2, '0')}:${checkOut.getMinutes().toString().padStart(2, '0')}`;
+    }
+    
+    if (day.attendance.projectName) {
+      tooltip += `\n项目: ${day.attendance.projectName}`;
+    }
+    
+    return tooltip;
+  }
+  
+  // 检查日期是否为今天
+  isToday(date: Date): boolean {
+    const today = new Date();
+    const checkDate = new Date(date);
+    return today.toDateString() === checkDate.toDateString();
+  }
+  
+  // 计算属性
+  filteredAttendance = computed(() => {
+    let filtered = this.attendanceData();
+    
+    // 按员工筛选
+    if (this.selectedEmployeeId()) {
+      filtered = filtered.filter(item => item.employeeId === this.selectedEmployeeId());
+    }
+    
+    // 按项目筛选
+    if (this.selectedProjectId()) {
+      filtered = filtered.filter(item => item.projectId === this.selectedProjectId());
+    }
+    
+    // 按视图筛选(日/周/月)
+    const selectedDate = this.selectedDate();
+    if (this.selectedView() === 'day') {
+      filtered = filtered.filter(item => {
+        const itemDate = new Date(item.date);
+        return itemDate.getDate() === selectedDate.getDate() &&
+               itemDate.getMonth() === selectedDate.getMonth() &&
+               itemDate.getFullYear() === selectedDate.getFullYear();
+      });
+    } else if (this.selectedView() === 'week') {
+      // 获取本周的起止日期
+      const startOfWeek = new Date(selectedDate);
+      const day = startOfWeek.getDay() || 7; // 调整周日为7
+      startOfWeek.setDate(startOfWeek.getDate() - day + 1);
+      const endOfWeek = new Date(startOfWeek);
+      endOfWeek.setDate(endOfWeek.getDate() + 6);
+      
+      filtered = filtered.filter(item => {
+        const itemDate = new Date(item.date);
+        return itemDate >= startOfWeek && itemDate <= endOfWeek;
+      });
+    } else if (this.selectedView() === 'month') {
+      filtered = filtered.filter(item => {
+        const itemDate = new Date(item.date);
+        return itemDate.getMonth() === selectedDate.getMonth() &&
+               itemDate.getFullYear() === selectedDate.getFullYear();
+      });
+    }
+    
+    return filtered;
+  });
+  
+  // 异常考勤列表
+  exceptionAttendance = computed(() => {
+    return this.filteredAttendance().filter(item => 
+      item.status === '迟到' || item.status === '早退' || item.status === '旷工'
+    ).sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
+  });
+  
+  // 考勤统计
+  attendanceStats = computed(() => {
+    const data = this.filteredAttendance();
+    const totalDays = data.length;
+    const normalDays = data.filter(item => item.status === '正常').length;
+    const lateDays = data.filter(item => item.status === '迟到').length;
+    const earlyLeaveDays = data.filter(item => item.status === '早退').length;
+    const absentDays = data.filter(item => item.status === '旷工').length;
+    const leaveDays = data.filter(item => item.status === '请假').length;
+    
+    const totalWorkHours = data.reduce((sum, item) => sum + item.workHours, 0);
+    const avgWorkHours = totalDays > 0 ? (totalWorkHours / (totalDays - leaveDays - absentDays || 1)).toFixed(1) : '0.0';
+    
+    return {
+      totalDays,
+      normalDays,
+      lateDays,
+      earlyLeaveDays,
+      absentDays,
+      leaveDays,
+      totalWorkHours: totalWorkHours.toFixed(1),
+      avgWorkHours,
+      complianceRate: totalDays > 0 ? Math.round((normalDays / totalDays) * 100) : 0
+    };
+  });
+  
+  // 部门考勤对比数据
+  departmentAttendanceData = computed(() => {
+    const departmentMap = new Map<string, { total: number; compliant: number }>();
+    
+    this.attendanceData().forEach(item => {
+      const employee = this.employees().find(emp => emp.id === item.employeeId);
+      if (employee) {
+        const dept = employee.department;
+        const current = departmentMap.get(dept) || { total: 0, compliant: 0 };
+        current.total++;
+        if (item.status === '正常') {
+          current.compliant++;
+        }
+        departmentMap.set(dept, current);
+      }
+    });
+    
+    const result: { department: string; complianceRate: number; total: number; compliant: number }[] = [];
+    departmentMap.forEach((value, key) => {
+      result.push({
+        department: key,
+        complianceRate: Math.round((value.compliant / value.total) * 100),
+        total: value.total,
+        compliant: value.compliant
+      });
+    });
+    
+    return result.sort((a, b) => b.complianceRate - a.complianceRate);
+  });
+  
+  // 显示的表格列
+  displayedColumns = ['date', 'employeeName', 'status', 'workHours', 'projectName', 'actions'];
+  
+  constructor(private dialog: MatDialog) {}
+  
+  ngOnInit() {
+    // 加载模拟数据
+    this.attendanceData.set(generateMockAttendanceData());
+    this.employees.set(generateMockEmployees());
+  }
+  
+  // 切换视图(日/周/月)
+  switchView(view: 'day' | 'week' | 'month') {
+    this.selectedView.set(view);
+  }
+  
+  // 获取员工姓名
+  getEmployeeName(employeeId: string): string {
+    const employee = this.employees().find(emp => emp.id === employeeId);
+    return employee ? employee.name : '未知员工';
+  }
+  
+  // 格式化日期
+  formatDate(date: Date | string): 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')}`;
+  }
+  
+  // 格式化时间
+  formatTime(date: Date | string): string {
+    if (!date) return '';
+    const d = new Date(date);
+    return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
+  }
+  
+  // 获取状态样式类
+  getStatusClass(status: string): string {
+    switch (status) {
+      case '正常':
+        return 'status-normal';
+      case '迟到':
+        return 'status-late';
+      case '早退':
+        return 'status-early';
+      case '旷工':
+        return 'status-absent';
+      case '请假':
+        return 'status-leave';
+      default:
+        return '';
+    }
+  }
+  
+  // 切换日期
+  navigateDate(direction: 'prev' | 'next') {
+    const newDate = new Date(this.selectedDate());
+    
+    if (this.selectedView() === 'day') {
+      newDate.setDate(newDate.getDate() + (direction === 'prev' ? -1 : 1));
+    } else if (this.selectedView() === 'week') {
+      newDate.setDate(newDate.getDate() + (direction === 'prev' ? -7 : 7));
+    } else if (this.selectedView() === 'month') {
+      newDate.setMonth(newDate.getMonth() + (direction === 'prev' ? -1 : 1));
+    }
+    
+    this.selectedDate.set(newDate);
+  }
+  
+  // 打开补卡申请对话框
+  openAttendanceDialog(attendance: AttendanceModel) {
+    const employee = this.employees().find(emp => emp.id === attendance.employeeId);
+    const dialogRef = this.dialog.open(AttendanceDialog, {
+      width: '500px',
+      maxWidth: '90vw',
+      disableClose: true,
+      data: {
+        ...attendance,
+        employeeName: employee ? employee.name : '未知员工'
+      }
+    });
+    
+    dialogRef.afterClosed().subscribe(result => {
+      if (result) {
+        // 在实际应用中,这里会提交补卡申请到服务器
+        alert('补卡申请已提交,等待审核');
+      }
+    });
+  }
+  
+  // 导出考勤数据
+  exportAttendanceData() {
+    alert('考勤数据导出功能待实现');
+  }
+  
+  // 获取日历数据
+  getCalendarDays() {
+    const year = this.selectedDate().getFullYear();
+    const month = this.selectedDate().getMonth();
+    
+    // 获取当月第一天和最后一天
+    const firstDay = new Date(year, month, 1);
+    const lastDay = new Date(year, month + 1, 0);
+    
+    // 获取当月第一天是星期几
+    const firstDayIndex = firstDay.getDay();
+    
+    const days = [];
+    
+    // 添加上月的最后几天
+    for (let i = firstDayIndex; i > 0; i--) {
+      const day = new Date(year, month, -i + 1);
+      days.push({
+        date: day,
+        dayOfMonth: day.getDate(),
+        currentMonth: false,
+        attendance: null
+      });
+    }
+    
+    // 添加当月的天数
+    for (let i = 1; i <= lastDay.getDate(); i++) {
+      const day = new Date(year, month, i);
+      const dayAttendance = this.attendanceData().filter(item => {
+        const itemDate = new Date(item.date);
+        return itemDate.getDate() === i &&
+               itemDate.getMonth() === month &&
+               itemDate.getFullYear() === year &&
+               (!this.selectedEmployeeId() || item.employeeId === this.selectedEmployeeId());
+      });
+      
+      days.push({
+        date: day,
+        dayOfMonth: i,
+        currentMonth: true,
+        attendance: dayAttendance.length > 0 ? dayAttendance[0] : null
+      });
+    }
+    
+    // 添加下月的前几天,凑够42天(6行7列)
+    const remainingDays = 42 - days.length;
+    for (let i = 1; i <= remainingDays; i++) {
+      const day = new Date(year, month + 1, i);
+      days.push({
+        date: day,
+        dayOfMonth: i,
+        currentMonth: false,
+        attendance: null
+      });
+    }
+    
+    return days;
+  }
+  
+  // 获取星期几的中文名称
+  getWeekdayName(index: number): string {
+    const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
+    return weekdays[index];
+  }
 }

+ 341 - 0
src/app/pages/hr/designer-profile/designer-profile.html

@@ -0,0 +1,341 @@
+<div class="designer-profile-container">
+  <header class="page-header">
+    <h1>设计师档案</h1>
+    <p class="page-description">查看设计师详细信息、作品集和项目历史</p>
+  </header>
+
+  <!-- 主内容区 -->
+  <div class="main-content">
+    <!-- 左侧:设计师信息概览区 -->
+    <div class="profile-sidebar">
+      <!-- 设计师基本信息 -->
+      <div class="profile-card">
+        <div class="avatar-container">
+          <img [src]="designer().avatar" alt="设计师头像" class="designer-avatar">
+          <div class="status-indicator online"></div>
+        </div>
+        <div class="profile-info">
+          <h2 class="designer-name">{{ designer().name }}</h2>
+          <p class="designer-position">{{ designer().position }}</p>
+          <p class="designer-department">{{ designer().department }}</p>
+        </div>
+        <div class="profile-stats">
+          <div class="stat-item">
+            <div class="stat-value">{{ ratingStats().totalProjects }}</div>
+            <div class="stat-label">已完成项目</div>
+          </div>
+          <div class="stat-item">
+            <div class="stat-value">{{ ratingStats().avgRating }}</div>
+            <div class="stat-label">平均评分</div>
+          </div>
+          <div class="stat-item">
+            <div class="stat-value">{{ formatDate(designer().hireDate) }}</div>
+            <div class="stat-label">入职日期</div>
+          </div>
+        </div>
+        <div class="contact-info">
+          <div class="contact-item">
+            <mat-icon>phone</mat-icon>
+            <span>{{ designer().phone }}</span>
+          </div>
+          <div class="contact-item">
+            <mat-icon>email</mat-icon>
+            <span>{{ designer().email }}</span>
+          </div>
+        </div>
+      </div>
+      
+      <!-- 技能雷达图 -->
+      <div class="skills-card">
+        <div class="card-header">
+          <h3>专业技能</h3>
+        </div>
+        <div class="radar-chart-container">
+          <!-- 简化版雷达图表示 -->
+          <div class="radar-chart">
+            <div class="radar-grid">
+              <!-- 这里应该是实际的雷达图,由于是静态模板,使用简化表示 -->
+              <div class="radar-point" style="--x: 80%; --y: 90%;" matTooltip="3D建模 (5)"></div>
+              <div class="radar-point" style="--x: 60%; --y: 85%;" matTooltip="渲染 (5)"></div>
+              <div class="radar-point" style="--x: 40%; --y: 80%;" matTooltip="空间设计 (4)"></div>
+              <div class="radar-point" style="--x: 30%; --y: 70%;" matTooltip="色彩搭配 (5)"></div>
+              <div class="radar-point" style="--x: 40%; --y: 40%;" matTooltip="CAD绘图 (4)"></div>
+              <div class="radar-point" style="--x: 60%; --y: 30%;" matTooltip="客户沟通 (3)"></div>
+              <div class="radar-point" style="--x: 80%; --y: 35%;" matTooltip="项目管理 (3)"></div>
+              <!-- 连接线 -->
+              <div class="radar-connect"></div>
+            </div>
+          </div>
+        </div>
+        <div class="skills-list">
+          <div *ngFor="let skill of skills()" class="skill-item" [class]="getSkillLevelClass(skill.level)">
+            <span class="skill-name">{{ skill.name }}</span>
+            <div class="skill-level">
+              <span *ngFor="let star of getStars(skill.level)" class="star">{{ star }}</span>
+            </div>
+          </div>
+        </div>
+      </div>
+      
+      <!-- 擅长领域 -->
+      <div class="specialties-card">
+        <div class="card-header">
+          <h3>擅长领域</h3>
+        </div>
+        <div class="specialties-cloud">
+          <div *ngFor="let specialty of specialties()" class="specialty-tag" [class.selected]="selectedSkills().includes(specialty.name)" (click)="toggleSkillFilter(specialty.name)">
+            <span class="specialty-name">{{ specialty.name }}</span>
+            <span class="specialty-count">{{ specialty.count }}</span>
+            <div class="specialty-level">
+              <div class="level-indicator" [style.width]="(specialty.level / 5 * 100) + '%'"></div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    
+    <!-- 右侧:作品集与项目历史区 -->
+    <div class="profile-content">
+      <!-- 标签页导航 -->
+      <div class="tabs-navigation">
+        <button 
+          mat-raised-button 
+          [class.active]="activeTab() === 'overview'"
+          (click)="switchTab('overview')"
+          class="tab-btn"
+        >
+          <mat-icon>dashboard</mat-icon>
+          概览
+        </button>
+        <button 
+          mat-raised-button 
+          [class.active]="activeTab() === 'portfolio'"
+          (click)="switchTab('portfolio')"
+          class="tab-btn"
+        >
+          <mat-icon>collections</mat-icon>
+          作品集
+        </button>
+        <button 
+          mat-raised-button 
+          [class.active]="activeTab() === 'projects'"
+          (click)="switchTab('projects')"
+          class="tab-btn"
+        >
+          <mat-icon>history</mat-icon>
+          项目历史
+        </button>
+      </div>
+      
+      <!-- 搜索和筛选 -->
+      <div class="search-filter-section">
+        <div class="search-container">
+          <mat-icon class="search-icon">search</mat-icon>
+          <input 
+            matInput 
+            placeholder="搜索作品或项目..."
+            [value]="searchTerm()"
+            (input)="searchTerm.set($event.target.value)"
+            class="search-input"
+          >
+          <button mat-icon-button *ngIf="searchTerm()" (click)="searchTerm.set('')" class="clear-search">
+            <mat-icon>close</mat-icon>
+          </button>
+        </div>
+      </div>
+      
+      <!-- 标签页内容 -->
+      <div class="tab-content">
+        <!-- 概览标签页 -->
+        <div *ngIf="activeTab() === 'overview'" class="overview-content">
+          <!-- 评分统计 -->
+          <div class="rating-stats-card">
+            <div class="card-header">
+              <h3>项目评分统计</h3>
+            </div>
+            <div class="rating-stats-content">
+              <div class="overall-rating">
+                <div class="rating-value">{{ ratingStats().avgRating }}</div>
+                <div class="rating-stars">
+                  <span *ngFor="let star of getStars(ratingStats().avgRating)" class="star">{{ star }}</span>
+                </div>
+                <div class="rating-count">基于 {{ ratingStats().totalProjects }} 个已完成项目</div>
+              </div>
+              <div class="rating-breakdown">
+                <div class="rating-item">
+                  <div class="rating-label">5星</div>
+                  <div class="rating-bar">
+                    <div class="rating-fill" [style.width]="(ratingStats().fiveStarCount / ratingStats().totalProjects * 100) + '%'"></div>
+                  </div>
+                  <div class="rating-percentage">{{ Math.round(ratingStats().fiveStarCount / ratingStats().totalProjects * 100) }}%</div>
+                </div>
+                <div class="rating-item">
+                  <div class="rating-label">4星</div>
+                  <div class="rating-bar">
+                    <div class="rating-fill" [style.width]="(ratingStats().fourStarCount / ratingStats().totalProjects * 100) + '%'"></div>
+                  </div>
+                  <div class="rating-percentage">{{ Math.round(ratingStats().fourStarCount / ratingStats().totalProjects * 100) }}%</div>
+                </div>
+                <div class="rating-item">
+                  <div class="rating-label">3星及以下</div>
+                  <div class="rating-bar">
+                    <div class="rating-fill" [style.width]="(ratingStats().threeStarCount / ratingStats().totalProjects * 100) + '%'"></div>
+                  </div>
+                  <div class="rating-percentage">{{ Math.round(ratingStats().threeStarCount / ratingStats().totalProjects * 100) }}%</div>
+                </div>
+              </div>
+            </div>
+          </div>
+          
+          <!-- 最近作品 -->
+          <div class="recent-portfolio-card">
+            <div class="card-header">
+              <h3>最近作品</h3>
+              <button mat-button (click)="switchTab('portfolio')" class="view-all-btn">
+                查看全部
+                <mat-icon>chevron_right</mat-icon>
+              </button>
+            </div>
+            <div class="recent-portfolio-grid">
+              <div 
+                *ngFor="let item of portfolio().slice(0, 4)"
+                class="portfolio-item-small"
+                (click)="openPortfolioPreview(item)"
+              >
+                <img [src]="item.imageUrl" alt="{{ item.title }}" class="portfolio-image-small">
+                <div class="portfolio-info-small">
+                  <div class="portfolio-title-small">{{ item.title }}</div>
+                  <div class="portfolio-rating-small">
+                    <span *ngFor="let star of getStars(item.rating)" class="star-small">{{ star }}</span>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+          
+          <!-- 进行中项目 -->
+          <div class="in-progress-card">
+            <div class="card-header">
+              <h3>进行中项目</h3>
+              <button mat-button (click)="switchTab('projects')" class="view-all-btn">
+                查看全部
+                <mat-icon>chevron_right</mat-icon>
+              </button>
+            </div>
+            <div class="projects-list">
+              <div 
+                *ngFor="let project of inProgressProjects()"
+                class="project-item"
+              >
+                <div class="project-info">
+                  <div class="project-name">{{ project.name }}</div>
+                  <div class="project-customer">客户: {{ project.customerName }}</div>
+                  <div class="project-time">开始于: {{ formatDate(project.startDate) }}</div>
+                </div>
+                <div class="project-role">
+                  {{ project.role }}
+                </div>
+                <div class="project-status" [class]="getProjectStatusClass(project.status)">
+                  {{ project.status }}
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+        
+        <!-- 作品集标签页 -->
+        <div *ngIf="activeTab() === 'portfolio'" class="portfolio-content">
+          <div class="portfolio-grid">
+            <div 
+              *ngFor="let item of filteredPortfolio()"
+              class="portfolio-item"
+              (click)="openPortfolioPreview(item)"
+            >
+              <div class="portfolio-image-container">
+                <img [src]="item.imageUrl" alt="{{ item.title }}" class="portfolio-image">
+                <div class="portfolio-overlay">
+                  <div class="portfolio-title">{{ item.title }}</div>
+                  <div class="portfolio-rating">
+                    <span *ngFor="let star of getStars(item.rating)" class="star">{{ star }}</span>
+                  </div>
+                </div>
+              </div>
+              <div class="portfolio-details">
+                <div class="portfolio-project-name">{{ item.projectName }}</div>
+                <div class="portfolio-date">{{ formatDate(item.completionDate) }}</div>
+              </div>
+            </div>
+          </div>
+          
+          <!-- 空状态 -->
+          <div *ngIf="filteredPortfolio().length === 0" class="empty-state">
+            <div class="empty-icon">
+              <mat-icon>search_off</mat-icon>
+            </div>
+            <p>没有找到符合条件的作品</p>
+            <button mat-button (click)="searchTerm.set(''); selectedSkills.set([])" class="clear-filter-btn">
+              清除筛选条件
+            </button>
+          </div>
+        </div>
+        
+        <!-- 项目历史标签页 -->
+        <div *ngIf="activeTab() === 'projects'" class="projects-content">
+          <!-- 项目时间轴 -->
+          <div class="project-timeline">
+            <div 
+              *ngFor="let project of orderedProjectHistory()"
+              class="timeline-item"
+            >
+              <div class="timeline-marker" [class]="getProjectStatusClass(project.status)"></div>
+              <div class="timeline-content">
+                <div class="timeline-date">{{ formatDate(project.startDate) }} - {{ project.endDate ? formatDate(project.endDate) : '进行中' }}</div>
+                <div class="project-card">
+                  <div class="project-header">
+                    <div class="project-name">{{ project.name }}</div>
+                    <div class="project-status" [class]="getProjectStatusClass(project.status)">
+                      {{ project.status }}
+                    </div>
+                  </div>
+                  <div class="project-details">
+                    <div class="detail-row">
+                      <span class="detail-label">客户:</span>
+                      <span class="detail-value">{{ project.customerName }}</span>
+                    </div>
+                    <div class="detail-row">
+                      <span class="detail-label">担任角色:</span>
+                      <span class="detail-value">{{ project.role }}</span>
+                    </div>
+                    <div *ngIf="project.rating" class="detail-row">
+                      <span class="detail-label">客户评分:</span>
+                      <span class="detail-value">
+                        <div class="rating">
+                          <span *ngFor="let star of getStars(project.rating)" class="star">{{ star }}</span>
+                        </div>
+                      </span>
+                    </div>
+                    <div *ngIf="project.feedback" class="detail-row">
+                      <span class="detail-label">客户反馈:</span>
+                      <span class="detail-value feedback" (click)="viewFeedback(project)">
+                        {{ project.feedback }}
+                        <mat-icon>expand_more</mat-icon>
+                      </span>
+                    </div>
+                    <div class="detail-row">
+                      <span class="detail-label">使用技能:</span>
+                      <div class="skills-used">
+                        <span *ngFor="let skill of project.skillsUsed" class="skill-used-tag">
+                          {{ skill }}
+                        </span>
+                      </div>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>

+ 1169 - 0
src/app/pages/hr/designer-profile/designer-profile.scss

@@ -0,0 +1,1169 @@
+// 自定义主题
+$primary-color: #1e40af; // 深蓝主色,传递可靠感
+$primary-light: #3b82f6; // 浅蓝色,用于悬停效果
+$secondary-color: #0d9488; // 薄荷绿,作为强调色
+$success-color: #10b981; // 成功色
+$warning-color: #f59e0b; // 警告色(浅橙)
+$error-color: #ef4444; // 错误色
+$info-color: #3b82f6; // 信息色
+$text-primary: #1f2937; // 主要文本色
+$text-secondary: #4b5563; // 次要文本色
+$text-tertiary: #9ca3af; // 辅助文本色
+$bg-primary: #ffffff; // 主背景色
+$bg-secondary: #f9fafb; // 次要背景色
+$bg-tertiary: #f3f4f6; // 辅助背景色
+$border-color: #e5e7eb; // 边框色
+$shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+$shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+$shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+$border-radius: 8px;
+$transition: all 0.2s ease;
+
+// 主容器样式
+.designer-profile-container {
+  padding: 24px;
+  min-height: 100vh;
+  background-color: $bg-secondary;
+  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+}
+
+// 页面标题
+.page-header {
+  margin-bottom: 24px;
+  padding-bottom: 16px;
+  border-bottom: 2px solid $border-color;
+
+  h1 {
+    font-size: 28px;
+    font-weight: 700;
+    color: $text-primary;
+    margin: 0 0 8px 0;
+  }
+
+  .page-description {
+    font-size: 16px;
+    color: $text-secondary;
+    margin: 0;
+  }
+}
+
+// 主内容区
+.main-content {
+  display: grid;
+  grid-template-columns: 400px 1fr;
+  gap: 24px;
+  align-items: start;
+}
+
+// 左侧:设计师信息概览区
+.profile-sidebar {
+  display: flex;
+  flex-direction: column;
+  gap: 24px;
+}
+
+// 设计师基本信息卡片
+.profile-card {
+  background-color: $bg-primary;
+  border-radius: $border-radius;
+  box-shadow: $shadow-sm;
+  padding: 24px;
+  text-align: center;
+
+  .avatar-container {
+    position: relative;
+    margin-bottom: 20px;
+
+    .designer-avatar {
+      width: 120px;
+      height: 120px;
+      border-radius: 50%;
+      object-fit: cover;
+      border: 4px solid $bg-secondary;
+      box-shadow: $shadow-md;
+      transition: $transition;
+
+      &:hover {
+        transform: scale(1.05);
+        box-shadow: $shadow-lg;
+      }
+    }
+
+    .status-indicator {
+      position: absolute;
+      bottom: 0;
+      right: 15px;
+      width: 24px;
+      height: 24px;
+      border-radius: 50%;
+      border: 3px solid $bg-primary;
+
+      &.online {
+        background-color: $success-color;
+      }
+    }
+  }
+
+  .profile-info {
+    margin-bottom: 20px;
+
+    .designer-name {
+      font-size: 24px;
+      font-weight: 700;
+      color: $text-primary;
+      margin: 0 0 4px 0;
+    }
+
+    .designer-position {
+      font-size: 16px;
+      font-weight: 500;
+      color: $primary-color;
+      margin: 0 0 4px 0;
+    }
+
+    .designer-department {
+      font-size: 14px;
+      color: $text-secondary;
+      margin: 0;
+    }
+  }
+
+  .profile-stats {
+    display: grid;
+    grid-template-columns: repeat(3, 1fr);
+    gap: 12px;
+    margin-bottom: 20px;
+    padding: 16px;
+    background-color: $bg-tertiary;
+    border-radius: $border-radius;
+
+    .stat-item {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+
+      .stat-value {
+        font-size: 20px;
+        font-weight: 700;
+        color: $text-primary;
+        margin-bottom: 4px;
+      }
+
+      .stat-label {
+        font-size: 12px;
+        color: $text-secondary;
+      }
+    }
+  }
+
+  .contact-info {
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+
+    .contact-item {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      gap: 8px;
+      font-size: 14px;
+      color: $text-secondary;
+
+      mat-icon {
+        font-size: 16px;
+      }
+    }
+  }
+}
+
+// 技能卡片
+.skills-card {
+  background-color: $bg-primary;
+  border-radius: $border-radius;
+  box-shadow: $shadow-sm;
+  padding: 24px;
+
+  .card-header {
+    margin-bottom: 20px;
+
+    h3 {
+      font-size: 20px;
+      font-weight: 600;
+      color: $text-primary;
+      margin: 0;
+    }
+  }
+
+  .radar-chart-container {
+    height: 200px;
+    margin-bottom: 20px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+
+  .radar-chart {
+    width: 180px;
+    height: 180px;
+    position: relative;
+    background-color: $bg-tertiary;
+    border-radius: 50%;
+    overflow: hidden;
+
+    .radar-grid {
+      position: absolute;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+    }
+
+    .radar-point {
+      position: absolute;
+      width: 12px;
+      height: 12px;
+      border-radius: 50%;
+      background-color: $primary-color;
+      transform: translate(-50%, -50%);
+      left: var(--x);
+      top: var(--y);
+      cursor: pointer;
+      transition: $transition;
+      z-index: 2;
+
+      &:hover {
+        transform: translate(-50%, -50%) scale(1.3);
+        box-shadow: 0 0 0 4px color-mix(in srgb, $primary-color 20%, transparent);
+      }
+    }
+
+    .radar-connect {
+      position: absolute;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      pointer-events: none;
+      // 这里应该是实际的连接线,但由于是静态模板,我们简化处理
+      background: radial-gradient(circle, color-mix(in srgb, $primary-color 10%, transparent) 0%, transparent 70%);
+    }
+  }
+
+  .skills-list {
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+
+    .skill-item {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 12px;
+      background-color: $bg-tertiary;
+      border-radius: $border-radius;
+      transition: $transition;
+
+      &:hover {
+        transform: translateX(4px);
+        background-color: color-mix(in srgb, $primary-color 5%, $bg-tertiary);
+      }
+
+      .skill-name {
+        font-size: 14px;
+        font-weight: 500;
+        color: $text-primary;
+      }
+
+      .skill-level {
+        display: flex;
+        gap: 2px;
+
+        .star {
+          font-size: 12px;
+          color: $warning-color;
+        }
+      }
+    }
+
+    .level-expert {
+      border-left: 3px solid $primary-color;
+    }
+
+    .level-advanced {
+      border-left: 3px solid $success-color;
+    }
+
+    .level-intermediate {
+      border-left: 3px solid $warning-color;
+    }
+
+    .level-beginner {
+      border-left: 3px solid $info-color;
+    }
+  }
+}
+
+// 擅长领域卡片
+.specialties-card {
+  background-color: $bg-primary;
+  border-radius: $border-radius;
+  box-shadow: $shadow-sm;
+  padding: 24px;
+
+  .card-header {
+    margin-bottom: 20px;
+
+    h3 {
+      font-size: 20px;
+      font-weight: 600;
+      color: $text-primary;
+      margin: 0;
+    }
+  }
+
+  .specialties-cloud {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+
+    .specialty-tag {
+      padding: 8px 16px;
+      background-color: $bg-tertiary;
+      border-radius: 20px;
+      cursor: pointer;
+      transition: $transition;
+      position: relative;
+      overflow: hidden;
+      display: flex;
+      align-items: center;
+      gap: 6px;
+
+      &:hover {
+        transform: scale(1.05);
+        background-color: color-mix(in srgb, $primary-color 10%, $bg-tertiary);
+      }
+
+      &.selected {
+        background-color: $primary-color;
+        color: white;
+        transform: scale(1.05);
+        box-shadow: $shadow-md;
+      }
+
+      .specialty-name {
+        font-size: 14px;
+        font-weight: 500;
+        z-index: 1;
+      }
+
+      .specialty-count {
+        font-size: 12px;
+        opacity: 0.7;
+        z-index: 1;
+      }
+
+      .specialty-level {
+        position: absolute;
+        bottom: 0;
+        left: 0;
+        width: 100%;
+        height: 3px;
+        background-color: transparent;
+
+        .level-indicator {
+          height: 100%;
+          background-color: $primary-color;
+          transition: width 0.5s ease;
+        }
+
+        .specialty-tag.selected & .level-indicator {
+          background-color: white;
+        }
+      }
+    }
+  }
+}
+
+// 右侧:作品集与项目历史区
+.profile-content {
+  display: flex;
+  flex-direction: column;
+  gap: 24px;
+}
+
+// 标签页导航
+.tabs-navigation {
+  display: flex;
+  gap: 8px;
+  background-color: $bg-primary;
+  border-radius: $border-radius;
+  padding: 4px;
+  box-shadow: $shadow-sm;
+  align-self: flex-start;
+}
+
+.tab-btn {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 12px 24px;
+  border-radius: $border-radius;
+  font-size: 14px;
+  font-weight: 500;
+  transition: $transition;
+
+  &.active {
+    background-color: $primary-color;
+    color: white;
+    box-shadow: $shadow-md;
+  }
+
+  &:not(.active) {
+    background-color: transparent;
+    color: $text-secondary;
+
+    &:hover {
+      background-color: $bg-tertiary;
+      color: $text-primary;
+    }
+  }
+}
+
+// 搜索和筛选区域
+.search-filter-section {
+  background-color: $bg-primary;
+  border-radius: $border-radius;
+  box-shadow: $shadow-sm;
+  padding: 16px 20px;
+
+  .search-container {
+    position: relative;
+    display: flex;
+    align-items: center;
+    max-width: 400px;
+
+    .search-icon {
+      position: absolute;
+      left: 12px;
+      color: $text-tertiary;
+      z-index: 1;
+    }
+
+    .search-input {
+      width: 100%;
+      padding: 10px 12px 10px 40px;
+      border: 1px solid $border-color;
+      border-radius: $border-radius;
+      font-size: 14px;
+      color: $text-primary;
+      background-color: $bg-primary;
+      transition: $transition;
+
+      &:focus {
+        outline: none;
+        border-color: $primary-color;
+        box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.1);
+      }
+
+      &::placeholder {
+        color: $text-tertiary;
+      }
+    }
+
+    .clear-search {
+      position: absolute;
+      right: 8px;
+      color: $text-tertiary;
+      padding: 4px;
+      transition: $transition;
+
+      &:hover {
+        color: $text-primary;
+      }
+    }
+  }
+}
+
+// 标签页内容
+.tab-content {
+  background-color: $bg-primary;
+  border-radius: $border-radius;
+  box-shadow: $shadow-sm;
+  padding: 24px;
+  min-height: 500px;
+}
+
+// 概览标签页
+.overview-content {
+  display: flex;
+  flex-direction: column;
+  gap: 24px;
+}
+
+// 评分统计卡片
+.rating-stats-card {
+  background-color: $bg-tertiary;
+  border-radius: $border-radius;
+  padding: 24px;
+
+  .card-header {
+    margin-bottom: 24px;
+
+    h3 {
+      font-size: 20px;
+      font-weight: 600;
+      color: $text-primary;
+      margin: 0;
+    }
+  }
+
+  .rating-stats-content {
+    display: grid;
+    grid-template-columns: 1fr 2fr;
+    gap: 32px;
+    align-items: center;
+
+    .overall-rating {
+      text-align: center;
+      padding: 20px;
+      background-color: $bg-primary;
+      border-radius: $border-radius;
+      box-shadow: $shadow-sm;
+
+      .rating-value {
+        font-size: 48px;
+        font-weight: 700;
+        color: $primary-color;
+        margin-bottom: 8px;
+        line-height: 1;
+      }
+
+      .rating-stars {
+        margin-bottom: 8px;
+
+        .star {
+          font-size: 18px;
+          color: $warning-color;
+          margin: 0 2px;
+        }
+      }
+
+      .rating-count {
+        font-size: 14px;
+        color: $text-secondary;
+      }
+    }
+
+    .rating-breakdown {
+      display: flex;
+      flex-direction: column;
+      gap: 16px;
+
+      .rating-item {
+        display: flex;
+        align-items: center;
+        gap: 12px;
+
+        .rating-label {
+          width: 60px;
+          font-size: 14px;
+          color: $text-secondary;
+        }
+
+        .rating-bar {
+          flex: 1;
+          height: 8px;
+          background-color: $bg-primary;
+          border-radius: 4px;
+          overflow: hidden;
+          position: relative;
+        }
+
+        .rating-fill {
+          height: 100%;
+          background-color: $primary-color;
+          border-radius: 4px;
+          transition: width 0.8s ease-out;
+          position: relative;
+          overflow: hidden;
+
+          &::after {
+            content: '';
+            position: absolute;
+            top: 0;
+            left: 0;
+            right: 0;
+            bottom: 0;
+            background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
+            animation: progressAnimation 1.5s infinite;
+          }
+        }
+
+        .rating-percentage {
+          width: 60px;
+          font-size: 14px;
+          font-weight: 500;
+          color: $text-primary;
+          text-align: right;
+        }
+      }
+    }
+  }
+}
+
+// 最近作品卡片
+.recent-portfolio-card {
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 20px;
+
+    h3 {
+      font-size: 20px;
+      font-weight: 600;
+      color: $text-primary;
+      margin: 0;
+    }
+
+    .view-all-btn {
+      color: $primary-color;
+      font-weight: 500;
+      transition: $transition;
+
+      &:hover {
+        background-color: color-mix(in srgb, $primary-color 5%, transparent);
+      }
+    }
+  }
+
+  .recent-portfolio-grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+    gap: 16px;
+
+    .portfolio-item-small {
+      position: relative;
+      border-radius: $border-radius;
+      overflow: hidden;
+      box-shadow: $shadow-sm;
+      cursor: pointer;
+      transition: $transition;
+
+      &:hover {
+        transform: translateY(-4px);
+        box-shadow: $shadow-md;
+      }
+
+      .portfolio-image-small {
+        width: 100%;
+        height: 140px;
+        object-fit: cover;
+      }
+
+      .portfolio-info-small {
+        padding: 12px;
+        background-color: $bg-primary;
+
+        .portfolio-title-small {
+          font-size: 14px;
+          font-weight: 500;
+          color: $text-primary;
+          margin-bottom: 4px;
+        }
+
+        .portfolio-rating-small {
+          .star-small {
+            font-size: 12px;
+            color: $warning-color;
+            margin: 0 1px;
+          }
+        }
+      }
+    }
+  }
+}
+
+// 进行中项目卡片
+.in-progress-card {
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 20px;
+
+    h3 {
+      font-size: 20px;
+      font-weight: 600;
+      color: $text-primary;
+      margin: 0;
+    }
+
+    .view-all-btn {
+      color: $primary-color;
+      font-weight: 500;
+      transition: $transition;
+
+      &:hover {
+        background-color: color-mix(in srgb, $primary-color 5%, transparent);
+      }
+    }
+  }
+
+  .projects-list {
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+
+    .project-item {
+      display: flex;
+      align-items: center;
+      gap: 20px;
+      padding: 16px;
+      background-color: $bg-tertiary;
+      border-radius: $border-radius;
+      transition: $transition;
+
+      &:hover {
+        transform: translateX(4px);
+        box-shadow: $shadow-sm;
+      }
+
+      .project-info {
+        flex: 1;
+
+        .project-name {
+          font-size: 16px;
+          font-weight: 500;
+          color: $text-primary;
+          margin-bottom: 4px;
+        }
+
+        .project-customer,
+        .project-time {
+          font-size: 12px;
+          color: $text-secondary;
+          margin: 0;
+        }
+      }
+
+      .project-role {
+        font-size: 14px;
+        color: $primary-color;
+        font-weight: 500;
+      }
+
+      .project-status {
+        padding: 6px 12px;
+        border-radius: 16px;
+        font-size: 12px;
+        font-weight: 500;
+        text-align: center;
+        min-width: 80px;
+      }
+
+      .status-completed {
+        background-color: color-mix(in srgb, $success-color 15%, transparent);
+        color: $success-color;
+      }
+
+      .status-in-progress {
+        background-color: color-mix(in srgb, $primary-color 15%, transparent);
+        color: $primary-color;
+      }
+    }
+  }
+}
+
+// 作品集标签页
+.portfolio-content {
+  .portfolio-grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+    gap: 24px;
+
+    .portfolio-item {
+      border-radius: $border-radius;
+      overflow: hidden;
+      box-shadow: $shadow-sm;
+      background-color: $bg-tertiary;
+      cursor: pointer;
+      transition: $transition;
+
+      &:hover {
+        transform: translateY(-4px);
+        box-shadow: $shadow-md;
+      }
+
+      .portfolio-image-container {
+        position: relative;
+        height: 200px;
+        overflow: hidden;
+
+        .portfolio-image {
+          width: 100%;
+          height: 100%;
+          object-fit: cover;
+          transition: $transition;
+
+          &:hover {
+            transform: scale(1.05);
+          }
+        }
+
+        .portfolio-overlay {
+          position: absolute;
+          bottom: 0;
+          left: 0;
+          right: 0;
+          background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent);
+          padding: 20px;
+          opacity: 0;
+          transition: $transition;
+
+          &:hover {
+            opacity: 1;
+          }
+
+          .portfolio-title {
+            font-size: 18px;
+            font-weight: 600;
+            color: white;
+            margin-bottom: 8px;
+          }
+
+          .portfolio-rating {
+            .star {
+              font-size: 16px;
+              color: $warning-color;
+              margin: 0 2px;
+            }
+          }
+        }
+      }
+
+      .portfolio-details {
+        padding: 16px;
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+
+        .portfolio-project-name {
+          font-size: 16px;
+          font-weight: 500;
+          color: $text-primary;
+        }
+
+        .portfolio-date {
+          font-size: 14px;
+          color: $text-secondary;
+        }
+      }
+    }
+  }
+
+  .empty-state {
+    text-align: center;
+    padding: 60px 20px;
+
+    .empty-icon {
+      font-size: 48px;
+      color: $text-tertiary;
+      margin-bottom: 16px;
+    }
+
+    p {
+      color: $text-secondary;
+      font-size: 16px;
+      margin-bottom: 24px;
+    }
+
+    .clear-filter-btn {
+      background-color: $primary-color;
+      color: white;
+      transition: $transition;
+
+      &:hover {
+        background-color: $primary-light;
+      }
+    }
+  }
+}
+
+// 项目历史标签页
+.projects-content {
+  .project-timeline {
+    position: relative;
+    padding-left: 32px;
+
+    // 时间轴线
+    &::before {
+      content: '';
+      position: absolute;
+      left: 14px;
+      top: 0;
+      bottom: 0;
+      width: 2px;
+      background-color: $border-color;
+    }
+
+    .timeline-item {
+      position: relative;
+      margin-bottom: 32px;
+      animation: fadeInUp 0.5s ease-out;
+      animation-fill-mode: both;
+
+      &:nth-child(1) {
+        animation-delay: 0.1s;
+      }
+      &:nth-child(2) {
+        animation-delay: 0.2s;
+      }
+      &:nth-child(3) {
+        animation-delay: 0.3s;
+      }
+      &:nth-child(4) {
+        animation-delay: 0.4s;
+      }
+
+      .timeline-marker {
+        position: absolute;
+        left: -32px;
+        top: 0;
+        width: 12px;
+        height: 12px;
+        border-radius: 50%;
+        background-color: $primary-color;
+        border: 3px solid $bg-primary;
+        box-shadow: 0 0 0 2px $border-color;
+        z-index: 1;
+      }
+
+      .timeline-date {
+        font-size: 14px;
+        color: $text-tertiary;
+        margin-bottom: 12px;
+        font-weight: 500;
+      }
+
+      .project-card {
+        background-color: $bg-tertiary;
+        border-radius: $border-radius;
+        padding: 20px;
+        transition: $transition;
+
+        &:hover {
+          transform: translateX(8px);
+          box-shadow: $shadow-md;
+        }
+
+        .project-header {
+          display: flex;
+          justify-content: space-between;
+          align-items: center;
+          margin-bottom: 16px;
+
+          .project-name {
+            font-size: 18px;
+            font-weight: 600;
+            color: $text-primary;
+            margin: 0;
+          }
+
+          .project-status {
+            padding: 6px 12px;
+            border-radius: 16px;
+            font-size: 12px;
+            font-weight: 500;
+            text-align: center;
+            min-width: 80px;
+          }
+
+          .status-completed {
+            background-color: color-mix(in srgb, $success-color 15%, transparent);
+            color: $success-color;
+          }
+
+          .status-in-progress {
+            background-color: color-mix(in srgb, $primary-color 15%, transparent);
+            color: $primary-color;
+          }
+        }
+
+        .project-details {
+          display: flex;
+          flex-direction: column;
+          gap: 12px;
+
+          .detail-row {
+            display: flex;
+            align-items: flex-start;
+            gap: 16px;
+
+            .detail-label {
+              width: 100px;
+              font-size: 14px;
+              font-weight: 500;
+              color: $text-secondary;
+            }
+
+            .detail-value {
+              flex: 1;
+              font-size: 14px;
+              color: $text-primary;
+
+              &.feedback {
+                cursor: pointer;
+                position: relative;
+                display: flex;
+                align-items: center;
+                gap: 8px;
+                color: $primary-color;
+                font-weight: 500;
+                transition: $transition;
+
+                &:hover {
+                  text-decoration: underline;
+                }
+
+                mat-icon {
+                  font-size: 14px;
+                  transition: $transition;
+                }
+              }
+
+              .rating {
+                display: flex;
+                gap: 2px;
+
+                .star {
+                  font-size: 14px;
+                  color: $warning-color;
+                }
+              }
+            }
+
+            .skills-used {
+              display: flex;
+              flex-wrap: wrap;
+              gap: 6px;
+              flex: 1;
+
+              .skill-used-tag {
+                padding: 4px 12px;
+                background-color: $bg-primary;
+                border-radius: 12px;
+                font-size: 12px;
+                color: $text-secondary;
+                transition: $transition;
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+// 动画定义
+@keyframes progressAnimation {
+  0% {
+    transform: translateX(-100%);
+  }
+  100% {
+    transform: translateX(100%);
+  }
+}
+
+@keyframes fadeInUp {
+  from {
+    opacity: 0;
+    transform: translateY(20px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+// 响应式设计
+@media (max-width: 1200px) {
+  .designer-profile-container {
+    padding: 16px;
+  }
+
+  .main-content {
+    grid-template-columns: 1fr;
+  }
+
+  .profile-sidebar {
+    flex-direction: row;
+    flex-wrap: wrap;
+  }
+
+  .profile-card,
+  .skills-card,
+  .specialties-card {
+    flex: 1;
+    min-width: 300px;
+  }
+}
+
+@media (max-width: 768px) {
+  .designer-profile-container {
+    padding: 12px;
+  }
+
+  .page-header {
+    text-align: center;
+  }
+
+  .profile-sidebar {
+    flex-direction: column;
+  }
+
+  .rating-stats-content {
+    grid-template-columns: 1fr;
+    gap: 24px;
+  }
+
+  .recent-portfolio-grid,
+  .portfolio-grid {
+    grid-template-columns: 1fr;
+  }
+
+  .tabs-navigation {
+    flex-wrap: wrap;
+    justify-content: center;
+  }
+
+  .project-timeline {
+    padding-left: 24px;
+
+    &::before {
+      left: 10px;
+    }
+
+    .timeline-item {
+      .timeline-marker {
+        left: -24px;
+      }
+
+      .project-card {
+        padding: 16px;
+      }
+
+      .detail-row {
+        flex-direction: column;
+        gap: 8px;
+
+        .detail-label {
+          width: auto;
+        }
+      }
+    }
+  }
+}

+ 498 - 0
src/app/pages/hr/designer-profile/designer-profile.ts

@@ -0,0 +1,498 @@
+import { Component, OnInit, signal, computed, Inject } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { MatButtonModule } from '@angular/material/button';
+import { MatCardModule } from '@angular/material/card';
+import { MatIconModule } from '@angular/material/icon';
+import { MatDialogModule, MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { MatTabsModule } from '@angular/material/tabs';
+import { MatTooltipModule } from '@angular/material/tooltip';
+import { Employee, DesignerSkill, DesignerPortfolioItem } from '../../../models/hr.model';
+
+// 作品集预览对话框组件
+@Component({
+  selector: 'app-portfolio-preview-dialog',
+  standalone: true,
+  imports: [
+    CommonModule,
+    MatButtonModule,
+    MatIconModule
+  ],
+  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>
+        </button>
+      </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>
+        </div>
+        <div class="detail-item">
+          <label>描述:</label>
+          <p>{{ portfolioItem.description }}</p>
+        </div>
+      </div>
+    </div>
+  `,
+  styles: [`
+    .dialog-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 24px;
+      padding-bottom: 16px;
+      border-bottom: 1px solid #e5e7eb;
+    }
+    .close-btn {
+      background: none;
+      border: none;
+      cursor: pointer;
+      color: #6b7280;
+      padding: 4px;
+    }
+    .dialog-content {
+      max-width: 90vw;
+      max-height: 80vh;
+    }
+    .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-image {
+      max-width: 100%;
+      max-height: 500px;
+      object-fit: contain;
+      border-radius: 8px;
+    }
+    .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;
+    }
+    .next-btn {
+      right: 10px;
+    }
+    .portfolio-details {
+      display: flex;
+      flex-direction: column;
+      gap: 16px;
+    }
+    .detail-item {
+      display: flex;
+      flex-direction: column;
+      gap: 4px;
+    }
+    label {
+      font-weight: 500;
+      color: #374151;
+      font-size: 14px;
+    }
+    span {
+      color: #4b5563;
+      font-size: 16px;
+    }
+    .rating {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+    }
+    .star {
+      color: #f59e0b;
+      font-size: 16px;
+    }
+    .rating-number {
+      font-size: 14px;
+      color: #6b7280;
+    }
+    p {
+      color: #4b5563;
+      line-height: 1.6;
+    }
+  `]
+}) 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 = [
+    {
+      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: '设计效果非常满意,完全符合我的预期,沟通也很顺畅。'
+    },
+    {
+      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: '设计很专业,细节处理到位,就是交付时间稍微延迟了一点。'
+    },
+    {
+      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: '完美的极简风格,我非常喜欢,推荐给了我的朋友们。'
+    },
+    {
+      id: 'proj-history-004',
+      name: '办公室规划',
+      customerName: '钱七',
+      startDate: new Date(2023, 10, 1),
+      endDate: null,
+      status: '进行中',
+      rating: null,
+      role: '设计顾问',
+      skillsUsed: ['空间设计', '项目管理', '客户沟通'],
+      feedback: null
+    }
+  ];
+  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 }
+  ];
+};
+
+// 主组件
+@Component({
+  selector: 'app-designer-profile',
+  standalone: true,
+  imports: [
+    CommonModule,
+    FormsModule,
+    MatButtonModule,
+    MatCardModule,
+    MatIconModule,
+    MatDialogModule,
+    MatTabsModule,
+    MatTooltipModule
+  ],
+  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) {}
+  
+  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);
+  }
+  
+  // 格式化日期
+  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')}`;
+  }
+  
+  // 打开作品集预览对话框
+  openPortfolioPreview(item: DesignerPortfolioItem) {
+    this.dialog.open(PortfolioPreviewDialog, {
+      width: '90vw',
+      maxWidth: '1200px',
+      maxHeight: '90vh',
+      data: {
+        portfolioItem: item,
+        portfolioItems: this.portfolio()
+      }
+    });
+  }
+  
+  // 切换技能筛选
+  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';
+  }
+  
+  // 获取项目状态样式
+  getProjectStatusClass(status: string): string {
+    switch (status) {
+      case '已完成':
+        return 'status-completed';
+      case '进行中':
+        return 'status-in-progress';
+      default:
+        return '';
+    }
+  }
+  
+
+  
+  // 查看客户评价详情
+  viewFeedback(project: any) {
+    alert(`客户反馈: ${project.feedback}`);
+  }
+  
+  // 生成技能雷达图数据
+  getRadarChartData() {
+    // 在实际应用中,这里应该返回用于渲染雷达图的数据
+    // 这里仅返回原始技能数据,图表渲染将在前端完成
+    return this.skills().map(skill => ({
+      subject: skill.name,
+      A: skill.level,
+      fullMark: 5
+    }));
+  }
+}