Ver Fonte

Merge branch 'master' of http://git.fmode.cn:3000/nkkj/yss-project

徐福静0235668 há 2 dias atrás
pai
commit
ac1335fe76

+ 0 - 3
angular.json

@@ -23,9 +23,6 @@
             ],
             "tsConfig": "tsconfig.app.json",
             "inlineStyleLanguage": "scss",
-            "externalDependencies": [
-              "echarts"
-            ],
             "assets": [
               {
                 "glob": "**/*",

+ 4 - 2
src/app/app.routes.ts

@@ -1,5 +1,7 @@
 import { Routes } from '@angular/router';
 
+// 登录页
+import { LoginPage } from './pages/auth/login/login';
 
 // 客服页面
 import { CustomerServiceLayout } from './pages/customer-service/customer-service-layout/customer-service-layout';
@@ -154,7 +156,7 @@ export const routes: Routes = [
     ]
   },
 
-  // 默认路由重定向到客服工作台
-  { path: '', redirectTo: '/customer-service/dashboard', pathMatch: 'full' },
+  // 默认路由重定向到登录页
+  { path: '', component: LoginPage, pathMatch: 'full' },
   { path: '**', redirectTo: '/customer-service/dashboard' }
 ];

+ 1 - 1
src/app/pages/admin/dashboard/dashboard.ts

@@ -3,7 +3,7 @@ import { RouterModule } from '@angular/router';
 import { Subscription } from 'rxjs';
 import { signal, Component, OnInit, AfterViewInit, OnDestroy, computed } from '@angular/core';
 import { AdminDashboardService } from './dashboard.service';
-// 使用全局echarts变量,不导入模块
+import * as echarts from 'echarts';
 
 @Component({
   selector: 'app-admin-dashboard',

+ 2 - 87
src/app/pages/admin/system-settings/system-settings.html

@@ -1,91 +1,6 @@
+<!-- System Settings Page -->
 <div class="system-settings">
-  <!-- 引入ECharts的CDN -->
-  <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
-  <style>
-    /* 全局样式 */
-    :host {
-      display: block;
-      padding: 20px;
-      background-color: #f5f7fa;
-      min-height: 100vh;
-    }
-    
-    /* 统计卡片样式 */
-    .stats-cards {
-      display: grid;
-      grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
-      gap: 20px;
-      margin-bottom: 24px;
-    }
-    
-    .stat-card {
-      background: white;
-      padding: 20px;
-      border-radius: 8px;
-      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
-      transition: transform 0.3s, box-shadow 0.3s;
-    }
-    
-    .stat-card:hover {
-      transform: translateY(-2px);
-      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
-    }
-    
-    .stat-card .label {
-      font-size: 14px;
-      color: #666;
-      margin-bottom: 8px;
-    }
-    
-    .stat-card .value {
-      font-size: 24px;
-      font-weight: 600;
-      color: #333;
-      margin-bottom: 4px;
-    }
-    
-    .stat-card .unit {
-      font-size: 14px;
-      color: #999;
-    }
-    
-    /* 图表容器样式 */
-    .charts-container {
-      display: grid;
-      grid-template-columns: 1fr 1fr;
-      gap: 24px;
-      margin-bottom: 24px;
-    }
-    
-    .chart-wrapper {
-      background: white;
-      padding: 20px;
-      border-radius: 8px;
-      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
-    }
-    
-    .chart-container {
-      height: 300px;
-      margin-top: 16px;
-    }
-    
-    /* 响应式设计 */
-    @media (max-width: 1024px) {
-      .charts-container {
-        grid-template-columns: 1fr;
-      }
-      
-      .stats-cards {
-        grid-template-columns: repeat(2, 1fr);
-      }
-    }
-    
-    @media (max-width: 768px) {
-      .stats-cards {
-        grid-template-columns: 1fr;
-      }
-    }
-  </style>
+
   <!-- 页面标题 -->
   <div class="page-header">
     <h2 class="page-title">系统设置</h2>

+ 77 - 0
src/app/pages/admin/system-settings/system-settings.scss

@@ -965,4 +965,81 @@ $shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.
 
 .workflow-timeline .timeline-step .label {
   letter-spacing: 0.5px;
+}
+
+// 统计卡片区域(系统配置页)
+.stats-cards {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
+  gap: 20px;
+  margin-bottom: 24px;
+}
+
+.stat-card {
+  background: $bg-white;
+  padding: 20px;
+  border-radius: 12px;
+  box-shadow: $shadow-sm;
+  transition: transform 0.2s ease, box-shadow 0.2s ease;
+  border: 1px solid $border-color;
+
+  &:hover {
+    transform: translateY(-2px);
+    box-shadow: $shadow-md;
+  }
+
+  .label {
+    font-size: 13px;
+    color: $text-tertiary;
+    margin-bottom: 8px;
+  }
+
+  .value {
+    font-size: 24px;
+    font-weight: 600;
+    color: $text-primary;
+    margin-bottom: 4px;
+  }
+
+  .unit {
+    font-size: 12px;
+    color: $text-tertiary;
+  }
+}
+
+// 图表区域
+.charts-container {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 24px;
+  margin-bottom: 24px;
+}
+
+.chart-wrapper {
+  background: $bg-white;
+  padding: 20px;
+  border-radius: 12px;
+  box-shadow: $shadow-sm;
+  border: 1px solid $border-color;
+}
+
+.chart-container {
+  height: 300px;
+  margin-top: 12px;
+}
+
+// 响应式优化
+@media (max-width: 1024px) {
+  .charts-container {
+    grid-template-columns: 1fr;
+  }
+  .stats-cards {
+    grid-template-columns: repeat(2, 1fr);
+  }
+}
+
+@media (max-width: 768px) {
+  .stats-cards {
+    grid-template-columns: 1fr;
+  }
 }

+ 13 - 18
src/app/pages/admin/system-settings/system-settings.ts

@@ -1,11 +1,5 @@
-// 扩展Window接口以包含echarts属性
-declare global {
-  interface Window {
-    echarts: any;
-  }
-}
-
 import { Component, OnInit, AfterViewInit, ViewChild, ElementRef } from '@angular/core';
+import * as echarts from 'echarts';
 import { CommonModule, NgIf, NgForOf } from '@angular/common';
 import { RouterModule } from '@angular/router';
 import { FormsModule } from '@angular/forms';
@@ -80,7 +74,8 @@ interface PerformanceRule {
     MatMenuModule,
     MatCheckboxModule
   ],
-  templateUrl: './system-settings.html'
+  templateUrl: './system-settings.html',
+  styleUrls: ['./system-settings.scss']
 })
 export class SystemSettings implements OnInit {
   // 当前激活的标签页
@@ -796,8 +791,8 @@ export class SystemSettings implements OnInit {
   
   // 初始化数据使用情况图表
   initDataUsageChart(): void {
-    if (window['echarts'] && this.dataUsageChart) {
-      const chart = window['echarts'].init(this.dataUsageChart.nativeElement);
+    if (echarts && this.dataUsageChart) {
+      const chart = echarts.init(this.dataUsageChart.nativeElement);
       
       const option = {
         title: {
@@ -882,8 +877,8 @@ export class SystemSettings implements OnInit {
   
   // 初始化备份历史图表
   initBackupHistoryChart(): void {
-    if (window['echarts'] && this.backupHistoryChart) {
-      const chart = window['echarts'].init(this.backupHistoryChart.nativeElement);
+    if (echarts && this.backupHistoryChart) {
+      const chart = echarts.init(this.backupHistoryChart.nativeElement);
       
       const dates = this.backupHistory.map(item => item.date);
       const sizes = this.backupHistory.map(item => item.size);
@@ -954,7 +949,7 @@ export class SystemSettings implements OnInit {
               color: '#165DFF'
             },
             areaStyle: {
-              color: new window['echarts'].graphic.LinearGradient(0, 0, 0, 1, [
+              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                 {
                   offset: 0,
                   color: 'rgba(22, 93, 255, 0.3)'
@@ -987,8 +982,8 @@ export class SystemSettings implements OnInit {
   
   // 初始化系统性能图表
   initSystemPerformanceChart(): void {
-    if (window['echarts'] && this.systemPerformanceChart) {
-      const chart = window['echarts'].init(this.systemPerformanceChart.nativeElement);
+    if (echarts && this.systemPerformanceChart) {
+      const chart = echarts.init(this.systemPerformanceChart.nativeElement);
       
       const times = this.systemPerformance.map(item => item.time);
       const cpuUsage = this.systemPerformance.map(item => item.cpuUsage);
@@ -1038,7 +1033,7 @@ export class SystemSettings implements OnInit {
               color: '#165DFF'
             },
             areaStyle: {
-              color: new window['echarts'].graphic.LinearGradient(0, 0, 0, 1, [
+              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                 {
                   offset: 0,
                   color: 'rgba(22, 93, 255, 0.5)'
@@ -1059,7 +1054,7 @@ export class SystemSettings implements OnInit {
               color: '#722ED1'
             },
             areaStyle: {
-              color: new window['echarts'].graphic.LinearGradient(0, 0, 0, 1, [
+              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                 {
                   offset: 0,
                   color: 'rgba(114, 46, 209, 0.5)'
@@ -1080,7 +1075,7 @@ export class SystemSettings implements OnInit {
               color: '#F7BA1E'
             },
             areaStyle: {
-              color: new window['echarts'].graphic.LinearGradient(0, 0, 0, 1, [
+              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                 {
                   offset: 0,
                   color: 'rgba(247, 186, 30, 0.5)'

+ 58 - 0
src/app/pages/auth/login/login.html

@@ -0,0 +1,58 @@
+<div class="login-container">
+  <div class="login-card">
+    <div class="brand">
+      <div class="logo"></div>
+      <h1>欢迎使用 YSS 平台</h1>
+      
+    </div>
+
+    <form (ngSubmit)="signIn()" class="form" autocomplete="off">
+      <div class="form-row">
+        <label for="username">账号</label>
+        <input id="username" type="text" [(ngModel)]="username" name="username" placeholder="请输入账号" />
+      </div>
+      <div class="form-row">
+        <label for="password">密码</label>
+        <input id="password" type="password" [(ngModel)]="password" name="password" placeholder="请输入密码" />
+      </div>
+      <button class="ios-primary" type="submit" [disabled]="loading">{{ loading ? '登录中…' : '登录' }}</button>
+      <div class="error" *ngIf="error">{{ error }}</div>
+    </form>
+
+    <div class="quick-roles">
+      <div class="divider"><span>快速进入</span></div>
+      <div class="roles-grid">
+        <button class="role-btn cs" (click)="goToRole('customer-service')">
+          <span class="icon">💬</span>
+          <span>客服</span>
+        </button>
+        <button class="role-btn designer" (click)="goToRole('designer')">
+          <span class="icon">🎨</span>
+          <span>设计师</span>
+        </button>
+        <button class="role-btn tl" (click)="goToRole('team-leader')">
+          <span class="icon">👑</span>
+          <span>组长</span>
+        </button>
+        <button class="role-btn finance" (click)="goToRole('finance')">
+          <span class="icon">💰</span>
+          <span>财务</span>
+        </button>
+        <button class="role-btn hr" (click)="goToRole('hr')">
+          <span class="icon">🧑‍💼</span>
+          <span>人事/行政</span>
+        </button>
+        <button class="role-btn admin" (click)="goToRole('admin')">
+          <span class="icon">🛡️</span>
+          <span>管理员</span>
+        </button>
+      </div>
+    </div>
+
+    <footer class="footer">
+      <span>© {{ currentYear }} YSS</span>
+      <a href="javascript:void(0)">隐私</a>
+      <a href="javascript:void(0)">条款</a>
+    </footer>
+  </div>
+</div>

+ 150 - 0
src/app/pages/auth/login/login.scss

@@ -0,0 +1,150 @@
+@use 'sass:math';
+
+$bg: #f2f2f7;
+$text: #1c1c1e;
+$muted: #8e8e93;
+$primary: #007aff;
+$card: #ffffff;
+$border: #e5e5ea;
+$shadow: 0 8px 30px rgba(0,0,0,0.06);
+
+.login-container {
+  min-height: 100vh;
+  display: grid;
+  place-items: center;
+  background: linear-gradient(180deg, #f5f7fb, #eef1f7);
+  padding: 24px;
+}
+
+.login-card {
+  width: min(960px, 100%);
+  background: $card;
+  border-radius: 24px;
+  border: 1px solid $border;
+  box-shadow: $shadow;
+  padding: clamp(20px, 4vw, 40px);
+  display: grid;
+  grid-template-rows: auto auto auto auto;
+  gap: 24px;
+}
+
+.brand {
+  text-align: center;
+  .logo {
+    font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', 'Apple Color Emoji', 'Segoe UI Emoji';
+    font-size: 42px;
+    margin-bottom: 4px;
+  }
+  h1 { font-size: clamp(20px, 2vw, 26px); color: $text; margin: 0; }
+  p { color: $muted; margin: 6px 0 0; }
+}
+
+.form {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 16px 20px;
+
+  .form-row {
+    display: grid;
+    gap: 8px;
+
+    label { color: $text; font-weight: 600; }
+    input {
+      height: 44px;
+      border: 1px solid $border;
+      border-radius: 12px;
+      padding: 0 14px;
+      font-size: 16px;
+      outline: none;
+      background: $card;
+      transition: box-shadow .2s ease, border-color .2s ease;
+
+      &:focus {
+        border-color: $primary;
+        box-shadow: 0 0 0 4px color-mix(in srgb, $primary 12%, transparent);
+      }
+    }
+  }
+
+  .ios-primary {
+    grid-column: 1 / -1;
+    height: 46px;
+    border: none;
+    border-radius: 14px;
+    background: $primary;
+    color: white;
+    font-weight: 600;
+    letter-spacing: .3px;
+    cursor: pointer;
+    transition: transform .04s ease, filter .2s ease;
+
+    &:active { transform: scale(.998); }
+    &:disabled { filter: grayscale(.2) opacity(.8); cursor: not-allowed; }
+  }
+}
+
+.quick-roles {
+  .divider {
+    display: grid;
+    grid-template-columns: 1fr auto 1fr;
+    align-items: center;
+    gap: 12px;
+    color: $muted;
+
+    &::before, &::after {
+      content: '';
+      height: 1px;
+      background: $border;
+      display: block;
+    }
+
+    span { padding: 2px 8px; border-radius: 999px; background: #f7f7fa; font-size: 12px; }
+  }
+
+  .roles-grid {
+    margin-top: 12px;
+    display: grid;
+    grid-template-columns: repeat(6, 1fr);
+    gap: 10px;
+
+    .role-btn {
+      height: 64px;
+      border-radius: 14px;
+      border: 1px solid $border;
+      background: white;
+      display: grid;
+      grid-template-columns: auto 1fr;
+      align-items: center;
+      gap: 8px;
+      padding: 0 12px;
+      cursor: pointer;
+      transition: box-shadow .2s ease, transform .04s ease, border-color .2s ease;
+
+      .icon { font-size: 18px; }
+      span:last-child { font-weight: 600; color: $text; }
+
+      &:hover { border-color: color-mix(in srgb, $primary 20%, $border); box-shadow: 0 6px 18px rgba(0,0,0,.04); }
+      &:active { transform: translateY(1px); }
+    }
+  }
+}
+
+.footer {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+  justify-content: center;
+  color: $muted;
+  font-size: 12px;
+
+  a { color: inherit; text-decoration: none; }
+}
+
+@media (max-width: 920px) {
+  .form { grid-template-columns: 1fr; }
+  .roles-grid { grid-template-columns: repeat(3, 1fr) !important; }
+}
+
+@media (max-width: 520px) {
+  .roles-grid { grid-template-columns: repeat(2, 1fr) !important; }
+}

+ 55 - 0
src/app/pages/auth/login/login.ts

@@ -0,0 +1,55 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { Router, RouterModule } from '@angular/router';
+
+@Component({
+  selector: 'app-login',
+  standalone: true,
+  imports: [CommonModule, FormsModule, RouterModule],
+  templateUrl: './login.html',
+  styleUrls: ['./login.scss']
+})
+export class LoginPage {
+  username = '';
+  password = '';
+  loading = false;
+  error = '';
+  currentYear = new Date().getFullYear();
+
+  constructor(private router: Router) {}
+
+  async signIn() {
+    this.error = '';
+    this.loading = true;
+    try {
+      // 这里保留为演示登录流程的占位,不破坏现有业务逻辑
+      await new Promise(r => setTimeout(r, 400));
+      // 登录成功后可跳转到常用角色,实际项目中可根据权限返回跳转
+      await this.router.navigateByUrl('/customer-service/dashboard');
+    } catch (e) {
+      this.error = '登录失败,请重试';
+    } finally {
+      this.loading = false;
+    }
+  }
+
+  goToRole(role: 'customer-service' | 'designer' | 'team-leader' | 'finance' | 'hr' | 'admin') {
+    const map: Record<string, string> = {
+      'customer-service': '/customer-service/dashboard',
+      'designer': '/designer/dashboard',
+      'team-leader': '/team-leader/dashboard',
+      'finance': '/finance/dashboard',
+      'hr': '/hr/assets',
+      'admin': '/admin/dashboard'
+    };
+
+    // iOS 风格过渡动画(与项目内其他页面行为一致)
+    document.body.classList.add('ios-page-transition');
+    setTimeout(() => {
+      this.router.navigateByUrl(map[role]).finally(() => {
+        setTimeout(() => document.body.classList.remove('ios-page-transition'), 300);
+      });
+    }, 100);
+  }
+}

+ 2 - 1
src/app/pages/customer-service/consultation-order/consultation-order.scss

@@ -1,3 +1,4 @@
+@use 'sass:math';
 // iOS风格变量定义
 $primary-color: #007AFF;
 $primary-dark: #0062CC;
@@ -385,7 +386,7 @@ $card-padding: 16px;
   }
   
   + .card {
-    margin-top: -$grid-gap/2;
+    margin-top: math.div(-$grid-gap, 2);
     border-top: 0.5px solid $border-color;
     border-radius: 0 0 $border-radius $border-radius;
   }

+ 22 - 15
src/app/pages/customer-service/dashboard/dashboard.html

@@ -112,25 +112,32 @@
             <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>
+          <!-- 任务处理状态(使用本地变量 s 消除可选链告警) -->
+          <ng-container *ngIf="taskProcessingState()[task.id] as s">
+            <ng-container *ngIf="s.inProgress === true">
+              <div class="task-progress-container">
+                <div class="task-progress-bar" [style.width]="s.progress + '%'">
+                  <span class="task-progress-text">{{ s.progress }}%</span>
+                </div>
               </div>
-            </div>
+            </ng-container>
           </ng-container>
         </div>
             <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>
+          <ng-container *ngIf="taskProcessingState()[task.id] as s; else noTaskActionState">
+            <button 
+              class="btn-primary"
+              [class.processing]="s.inProgress === true"
+              (click)="handleAssignment(task.id)"
+              [disabled]="(s.inProgress === true) || task.isCompleted"
+            >
+              {{ s.inProgress === true ? '处理中' : '处理' }}
+            </button>
+          </ng-container>
+          <ng-template #noTaskActionState>
+            <button class="btn-primary" (click)="handleAssignment(task.id)" [disabled]="task.isCompleted">处理</button>
+          </ng-template>
+          </div>
       </div>
     </div>
   </section>

+ 1 - 1
src/app/pages/customer-service/dashboard/dashboard.ts

@@ -27,7 +27,7 @@ export class Dashboard implements OnInit, OnDestroy {
   urgentTasks = signal<Task[]>([]);
 
   // 任务处理状态
-  taskProcessingState = signal<{[key: string]: {inProgress: boolean, progress: number}}>({});
+  taskProcessingState = signal<Partial<Record<string, { inProgress: boolean; progress: number }>>>({});
   
   // 项目动态流
   projectUpdates = signal<(Project | CustomerFeedback)[]>([]);

+ 9 - 88
src/app/pages/customer-service/project-detail/project-detail.html

@@ -67,7 +67,12 @@
             <div class="consultation-item" *ngFor="let record of consultationRecords()">
               <div class="consultation-date">{{ formatDate(record.date) }}</div>
               <div class="consultation-content">{{ record.content }}</div>
-              <div class="consultation-status">{{ record.status }}</div>
+              <div class="consultation-status"
+                   [class.status-processed]="record.status === '已解决' || record.status === '成功'"
+                   [class.status-processing]="record.status === '处理中'"
+                   [class.status-pending]="record.status === '待处理'">
+                {{ record.status }}
+              </div>
             </div>
           </div>
         </div>
@@ -93,7 +98,7 @@
               <div class="feedback-date">{{ formatDate(feedback.date) }}</div>
               <div class="feedback-rating">
                 <span *ngFor="let star of [1,2,3,4,5]">
-                  <i class="fa" [ngClass]="{ 'fa-star': star <= feedback.rating, 'fa-star-o': star > feedback.rating }" style="color: #FFD700;"></i>
+                  <i class="fa" [ngClass]="{ 'fa-star': star <= feedback.rating, 'fa-star-o': star > feedback.rating }"></i>
                 </span>
               </div>
               <div class="feedback-content">{{ feedback.content }}</div>
@@ -267,7 +272,7 @@
               <h4 class="card-title">项目团队</h4>
               <div class="team-info">
                 <div class="team-member">
-                  <div style="width: 64px; height: 48px; background-color: #DCDCDC; color: #555555; display: flex; align-items: center; justify-content: center; font-size: 16px; font-weight: bold;" class="member-avatar" title="客服小李">IMG</div>
+                  <div class="member-avatar" title="客服小李">IMG</div>
                   <div class="member-details">
                     <div class="member-name">客服小李</div>
                     <div class="member-role">客户经理</div>
@@ -279,7 +284,7 @@
                   </button>
                 </div>
                 <div class="team-member">
-                  <div style="width: 91px; height: 48px; background-color: #DCDCDC; color: #555555; display: flex; align-items: center; justify-content: center; font-size: 16px; font-weight: bold;" class="member-avatar" title="张设计师">IMG</div>
+                  <div class="member-avatar" title="张设计师">IMG</div>
                   <div class="member-details">
                     <div class="member-name">张设计师</div>
                     <div class="member-role">主设计师</div>
@@ -431,90 +436,6 @@
                 </div>
               </div>
             </div>
-            <!-- 修复消息输入区域,避免重复定义 -->
-            <div class="message-input-area">
-            <!-- 只使用单向绑定 + input事件 -->
-            <textarea 
-              [value]="newMessage()"
-              (input)="onMessageInput($event)"
-              placeholder="输入消息内容..."
-              rows="3"
-              (keydown.enter.shift)="$event.preventDefault()"
-              (keydown.enter)="sendMessage()"
-            ></textarea>
-            <div class="message-actions">
-              <button class="secondary-btn">
-                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor">
-                  <path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path>
-                  <polyline points="14 2 14 8 20 8"></polyline>
-                </svg>
-                <span>上传文件</span>
-              </button>
-              <button class="primary-btn" (click)="sendMessage()" [disabled]="!newMessage().trim()">
-                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor">
-                  <line x1="22" y1="2" x2="11" y2="13"></line>
-                  <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
-                </svg>
-                <span>发送</span>
-              </button>
-            </div>
-            </div>
-          </div>
-        </div>
-
-        <!-- 文件标签内容 -->
-        <div *ngIf="activeTab() === 'files'" class="tab-content">
-          <div class="files-filter">
-            <div class="filter-options">
-              <button class="filter-btn active">全部文件</button>
-              <button class="filter-btn">文档</button>
-              <button class="filter-btn">图片</button>
-              <button class="filter-btn">表格</button>
-            </div>
-            <div class="search-box">
-              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
-                <circle cx="11" cy="11" r="8"></circle>
-                <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
-              </svg>
-              <input type="text" placeholder="搜索文件...">
-            </div>
-          </div>
-          <div class="files-list">
-            <div *ngFor="let file of files()" class="file-item">
-              <div class="file-icon" [class.type-document]="file.type === 'document'" [class.type-image]="file.type === 'image'" [class.type-spreadsheet]="file.type === 'spreadsheet'">
-                <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
-                  <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
-                  <polyline points="14 2 14 8 20 8"></polyline>
-                  <line x1="16" y1="13" x2="8" y2="13"></line>
-                  <line x1="16" y1="17" x2="8" y2="17"></line>
-                  <polyline points="10 9 9 9 8 9"></polyline>
-                </svg>
-              </div>
-              <div class="file-info">
-                <h4 class="file-name">{{ file.name }}</h4>
-                <div class="file-meta">
-                  <span class="file-uploader">上传者:{{ file.uploadedBy }}</span>
-                  <span class="file-date">{{ formatDate(file.uploadedAt) }}</span>
-                  <span class="file-size">{{ file.size }}</span>
-                </div>
-              </div>
-              <div class="file-actions">
-                <button class="action-btn btn-hover-effect" (click)="previewFile(file)" title="预览">
-                  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor">
-                    <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
-                  </svg>
-                </button>
-                <button class="action-btn btn-hover-effect" (click)="downloadFile(file)" title="下载">
-                  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor">
-                    <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
-                    <polyline points="7 10 12 15 17 10"></polyline>
-                    <line x1="12" y1="15" x2="12" y2="3"></line>
-                  </svg>
-                </button>
-              </div>
-            </div>
-          </div>
-        </div>
       </div>
     </div>
 

+ 488 - 17
src/app/pages/customer-service/project-detail/project-detail.scss

@@ -1,3 +1,5 @@
+@use "sass:color";
+
 // iOS风格全局变量
 $primary-color: #007AFF;
 $primary-dark: #0047AB;
@@ -1143,16 +1145,17 @@ $text-light: $text-tertiary;
     // 信息卡片
     .info-card {
       padding: 20px;
-      background-color: $bg-light;
-      border-radius: 8px;
+      background-color: color-mix(in srgb, $bg-light 80%, $bg-white);
+      border-radius: 10px;
 
       .card-title {
         font-size: 16px;
-        font-weight: 600;
+        font-weight: 700;
         color: $text-primary;
-        margin: 0 0 16px 0;
-        padding-bottom: 12px;
-        border-bottom: 1px solid $border-color;
+        margin: 0 0 14px 0;
+        padding-bottom: 10px;
+        border-bottom: 1px solid color-mix(in srgb, $border-color 80%, transparent);
+        letter-spacing: .2px;
       }
 
       // 客户信息
@@ -1163,14 +1166,18 @@ $text-light: $text-tertiary;
           label {
             display: block;
             font-size: 12px;
-            font-weight: 500;
-            color: $text-tertiary;
+            font-weight: 600;
+            color: $text-secondary;
+            letter-spacing: .2px;
             margin-bottom: 4px;
           }
 
           span {
             font-size: 14px;
+            font-weight: 500;
             color: $text-primary;
+            font-variant-numeric: tabular-nums;
+            letter-spacing: .1px;
           }
 
           .tag-container {
@@ -1179,11 +1186,13 @@ $text-light: $text-tertiary;
             flex-wrap: wrap;
 
             .tag {
-              padding: 4px 12px;
+              padding: 4px 10px;
               font-size: 12px;
-              color: $primary-color;
-              background-color: #e3f2fd;
-              border-radius: 16px;
+              font-weight: 600;
+              color: color.mix($primary-color, black, 85%);
+              background-color: color-mix(in srgb, #e3f2fd 80%, white);
+              border-radius: 14px;
+              letter-spacing: .2px;
             }
           }
 
@@ -1227,6 +1236,15 @@ $text-light: $text-tertiary;
             height: 48px;
             border-radius: 50%;
             object-fit: cover;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            background: linear-gradient(180deg, #f5f7fa, #e9edf3);
+            color: $text-secondary;
+            font-weight: 600;
+            font-size: 14px;
+            border: 1px solid $border-color;
+            box-shadow: 0 1px 2px rgba(0,0,0,0.04);
           }
 
           .member-details {
@@ -1234,15 +1252,16 @@ $text-light: $text-tertiary;
             margin-left: 12px;
 
             .member-name {
-              font-size: 14px;
-              font-weight: 500;
+              font-size: 15px;
+              font-weight: 600;
               color: $text-primary;
               margin-bottom: 2px;
+              letter-spacing: .2px;
             }
 
             .member-role {
               font-size: 12px;
-              color: $text-light;
+              color: $text-secondary;
             }
           }
 
@@ -1262,6 +1281,13 @@ $text-light: $text-tertiary;
             &:hover {
               background-color: $primary-color;
               color: white;
+              box-shadow: 0 8px 16px rgba(0,0,0,0.12);
+              transform: translateY(-1px);
+            }
+
+            &:active {
+              transform: translateY(0);
+              box-shadow: 0 4px 10px rgba(0,0,0,0.08);
             }
           }
         }
@@ -1292,6 +1318,11 @@ $text-light: $text-tertiary;
               align-items: center;
               gap: 8px;
 
+              .fa {
+                color: $warning-color;
+                margin-right: 2px;
+              }
+
               .rating-stars {
                 font-size: 12px;
                 color: $warning-color;
@@ -1307,8 +1338,8 @@ $text-light: $text-tertiary;
 
           .feedback-content {
             font-size: 14px;
-            color: $text-secondary;
-            line-height: 1.5;
+            color: $text-primary;
+            line-height: 1.6;
             margin-bottom: 8px;
           }
 
@@ -1961,4 +1992,444 @@ $text-light: $text-tertiary;
   .message-content {
     max-width: 85% !important;
   }
+}
+
+// Project Detail iOS tweaks
+.project-detail-container {
+  // 统一卡片间距(仅作用于本页)
+  .card {
+    padding: 20px;
+    margin-bottom: 24px;
+  }
+
+  // 标签页激活态(底部指示条 + 主色高亮)
+  .project-tabs {
+    .tab-header {
+      position: relative;
+    }
+    .tab-btn {
+      position: relative;
+    }
+    .tab-btn.active {
+      color: $primary-color;
+    }
+    .tab-btn.active::after {
+      content: '';
+      position: absolute;
+      left: 16px;
+      right: 16px;
+      bottom: -1px;
+      height: 2px;
+      background: $primary-color;
+      border-radius: 2px;
+    }
+  }
+
+  // 进度条填充质感(渐变 + 轻微内阴影)
+  .progress-bar {
+    .progress-fill {
+      background: linear-gradient(90deg, color.adjust($primary-color, $lightness: 8%), $primary-color);
+      border-radius: 4px;
+      box-shadow: inset 0 0 2px rgba(0,0,0,0.08);
+    }
+  }
+
+  // 输入框聚焦的环形高亮
+  .wechat-input-area {
+    .wechat-input:focus {
+      box-shadow: 0 0 0 3px color-mix(in srgb, $primary-color 15%, transparent);
+    }
+  }
+}
+
+// 响应式优化:侧栏在中等屏略窄,小屏隐藏
+@media (max-width: 1200px) {
+  .project-detail-container {
+    .wechat-sidebar { width: 300px; }
+  }
+}
+@media (max-width: 992px) {
+  .project-detail-container {
+    .wechat-sidebar { display: none; }
+    .project-content-main { width: 100%; }
+  }
+}
+
+// Project Detail iOS segmented & refinements (ABCD)
+.project-detail-container {
+  // A+B: 胶囊分段标签 + 细化激活态
+  .project-tabs.ios-tabs {
+    background: $background-primary;
+    border-radius: $border-radius;
+    box-shadow: $shadow-sm;
+    padding: 8px;
+
+    .tab-header {
+      background: $background-secondary;
+      border: 1px solid $border-color;
+      border-radius: $border-radius;
+      padding: 4px;
+      gap: 4px;
+    }
+
+    .tab-btn {
+      position: relative;
+      border-radius: $border-radius;
+      padding: 10px 14px;
+      font-size: 14px;
+      font-weight: 500;
+      color: $text-secondary;
+      transition: color .18s ease, background-color .18s ease, transform .18s ease;
+
+      &:hover { background-color: color-mix(in srgb, $primary-color 6%, transparent); }
+      &:active { transform: scale(0.98); }
+
+      &.active {
+        color: $primary-color;
+        font-weight: 600; // B: 激活时略增字重
+      }
+      &.active::before { // A: 胶囊式激活背景
+        content: '';
+        position: absolute;
+        inset: 0;
+        background: $background-primary;
+        border-radius: inherit;
+        box-shadow: 0 1px 2px rgba(0,0,0,0.06);
+        z-index: -1;
+      }
+      &.active::after { // B: 极细底线增强层次
+        content: '';
+        position: absolute;
+        left: 12px;
+        right: 12px;
+        bottom: 6px;
+        height: 2px;
+        border-radius: 2px;
+        background: $primary-color;
+        opacity: .7;
+      }
+    }
+  }
+
+  // C: 输入区细化
+  .wechat-input-area {
+    .wechat-input::placeholder { color: $text-tertiary; }
+
+    .action-btn:hover {
+      background-color: color-mix(in srgb, $text-primary 8%, transparent);
+      color: $text-primary;
+    }
+
+    .send-btn {
+      &:active:not(:disabled) { transform: scale(0.98); }
+      &:disabled {
+        background-color: $background-tertiary;
+        color: $text-tertiary;
+        border: 1px solid $border-color;
+        opacity: 0.9;
+        pointer-events: none;
+      }
+    }
+  }
+
+  // D: 侧栏与卡片阴影/圆角统一
+  .wechat-sidebar { box-shadow: $shadow-sm; }
+}
+
+@media (max-width: 768px) {
+  .project-detail-container {
+    .wechat-sidebar { border-radius: $border-radius; }
+  }
+}
+
+// Project Detail iOS buttons + history + bottom actions
+.project-detail-container {
+  // 顶部右侧两个按钮:导出报告(secondary)、联系客户(primary)
+  .header-actions {
+    .primary-btn,
+    .secondary-btn {
+      height: 36px;
+      padding: 0 14px;
+      border-radius: 10px;
+      font-size: 14px;
+      font-weight: 500;
+      letter-spacing: .2px;
+      display: inline-flex;
+      align-items: center;
+      gap: 8px;
+      transition: background-color .18s ease, color .18s ease, transform .12s ease;
+
+      svg { width: 16px; height: 16px; }
+
+      &:active { transform: scale(0.98); }
+    }
+
+    .secondary-btn {
+      color: $text-primary;
+      background-color: $bg-white;
+      border: 1px solid $border-color;
+
+      &:hover { background-color: $bg-light; }
+      &:focus-visible { box-shadow: 0 0 0 3px color-mix(in srgb, $text-primary 10%, transparent); }
+    }
+
+    .primary-btn {
+      color: #fff;
+      background-color: $primary-color;
+      border: 1px solid $primary-color;
+
+      &:hover { background-color: color.adjust($primary-color, $lightness: -6%); }
+      &:focus-visible { box-shadow: 0 0 0 3px color-mix(in srgb, $primary-color 20%, transparent); }
+      &:disabled { background: $text-tertiary; border-color: $text-tertiary; cursor: not-allowed; }
+    }
+  }
+
+  // 历史服务记录板块字体与排版
+  .historical-records-card {
+    .record-section {
+      .projects-list { display: grid; grid-template-columns: 1fr; gap: 10px; }
+      .project-item { padding: 10px 12px; border-radius: 8px; background: $bg-white; border: 1px solid $border-color; }
+      .project-name { font-size: 14px; font-weight: 600; color: $text-primary; margin-bottom: 4px; }
+      .project-period { font-size: 12px; color: $text-tertiary; margin-bottom: 6px; }
+      .project-description { font-size: 14px; color: $text-primary; line-height: 1.6; margin-bottom: 4px; }
+      .project-status { font-size: 12px; color: $text-secondary; }
+    }
+  }
+
+  // 底部售后操作栏(固定在底部的小界面):现代感与极简
+  .after-sales-actions.ios-actions {
+    position: sticky;
+    bottom: 0;
+    z-index: 5;
+    padding: 12px 0;
+
+    .actions-container {
+      display: flex;
+      gap: 12px;
+      justify-content: center;
+      padding: 10px 16px;
+      background: color-mix(in srgb, $bg-white 70%, transparent);
+      border-top: 1px solid $border-color;
+      backdrop-filter: saturate(160%) blur(10px);
+      -webkit-backdrop-filter: saturate(160%) blur(10px);
+      box-shadow: 0 -2px 8px rgba(0,0,0,0.04);
+      border-radius: 12px;
+    }
+
+    .primary-btn,
+    .secondary-btn,
+    .warning-btn {
+      height: 36px;
+      padding: 0 14px;
+      border-radius: 10px;
+      font-size: 14px;
+      font-weight: 500;
+      letter-spacing: .2px;
+      border: 1px solid transparent;
+      transition: background-color .18s ease, color .18s ease, transform .12s ease;
+      &:active { transform: scale(0.98); }
+    }
+
+    .primary-btn { color: #fff; background: $primary-color; border-color: $primary-color; &:hover { background: color.adjust($primary-color, $lightness: -6%); } }
+    .secondary-btn { color: $text-primary; background: $bg-white; border-color: $border-color; &:hover { background: $bg-light; } }
+    .warning-btn { color: #8a6100; background: #fff8e1; border-color: #ffe0a5; &:hover { background: #ffefc2; } }
+  }
+
+  // 里程碑小界面下方的动作区
+  .milestone-actions {
+    display: flex;
+    gap: 8px;
+    padding-top: 8px;
+    border-top: 1px dashed $border-color;
+
+    .primary-btn.small {
+      border-radius: 10px;
+      padding: 6px 12px;
+      font-weight: 600;
+    }
+  }
+}
+
+// 移动端微调
+@media (max-width: 768px) {
+  .project-detail-container {
+    .after-sales-actions.ios-actions .actions-container { border-radius: 10px; gap: 8px; }
+    .historical-records-card .record-section .consultation-item { padding: 10px; }
+  }
+}
+
+// iOS micro tuning — apply all requested adjustments
+.project-detail-container {
+  // Header actions buttons
+  .header-actions {
+    .primary-btn,
+    .secondary-btn {
+      height: 40px;
+      padding: 0 16px;
+      border-radius: 12px;
+    }
+    .primary-btn:hover {
+      background-color: color.adjust($primary-color, $lightness: -4%);
+    }
+  }
+
+  // Historical records cards — softer borders
+  .historical-records-card .record-section {
+    .consultation-item,
+    .cooperation-item,
+    .feedback-item,
+    .project-item {
+      border-color: color-mix(in srgb, $border-color 70%, transparent);
+    }
+  }
+
+  // Bottom sticky actions — lighter frosted glass and rounded container
+  .after-sales-actions.ios-actions {
+    .actions-container {
+      background: color-mix(in srgb, $bg-white 60%, transparent);
+      backdrop-filter: saturate(160%) blur(8px);
+      -webkit-backdrop-filter: saturate(160%) blur(8px);
+      border-radius: 14px;
+    }
+    .warning-btn {
+      color: #7a5200;
+      background: #fff5d6;
+      border-color: #ffd88a;
+      &:hover { background: #ffedbd; }
+    }
+  }
+
+  // Milestone small actions
+  .milestone-actions .primary-btn.small {
+    border-radius: 12px;
+    font-size: 13px;
+  }
+}
+
+@media (max-width: 768px) {
+  .project-detail-container {
+    .header-actions {
+      .primary-btn,
+      .secondary-btn { height: 38px; padding: 0 14px; }
+    }
+    .after-sales-actions.ios-actions .actions-container { border-radius: 12px; }
+  }
+}
+
+// Override: 历史卡片改为无描边 + 极浅阴影(高优先级覆盖)
+.project-detail-container {
+  .historical-records-card .record-section {
+    .consultation-item,
+    .cooperation-item,
+    .feedback-item,
+    .project-item {
+      border: none !important;
+      box-shadow: 0 1px 3px rgba(0,0,0,0.04);
+    }
+  }
+}
+
+@media (max-width: 768px) {
+  .project-detail-container {
+    .historical-records-card .record-section {
+      .consultation-item,
+      .cooperation-item,
+      .feedback-item,
+      .project-item {
+        box-shadow: 0 1px 2px rgba(0,0,0,0.04);
+      }
+    }
+  }
+}
+
+// Typography & layout improvements for Historical Records — 更直观的数据层级
+.project-detail-container {
+  .historical-records-card {
+    .record-section {
+      h4 { font-size: 16px; font-weight: 600; color: $text-primary; margin-bottom: 10px; }
+
+      // 过往咨询记录:日期-内容-状态 三段式布局
+      .consultation-list {
+        .consultation-item {
+          display: grid;
+          grid-template-columns: 128px 1fr auto; // 日期 | 内容 | 状态
+          gap: 8px 12px;
+          align-items: center;
+
+          .consultation-date {
+            font-size: 12px;
+            color: $text-tertiary;
+            letter-spacing: .2px;
+            font-variant-numeric: tabular-nums;
+          }
+          .consultation-content {
+            font-size: 14px;
+            color: $text-primary;
+            line-height: 1.6;
+          }
+          .consultation-status {
+            justify-self: end;
+            font-size: 12px;
+            font-weight: 600;
+            color: $text-primary;
+            background: $bg-light;
+            padding: 2px 8px;
+            border-radius: 10px;
+
+            // 三色状态:成功/处理中/待处理
+            &.status-processed { color: $success-color; background-color: #e8f5e9; }
+            &.status-processing { color: $primary-color; background-color: #e3f2fd; }
+            &.status-pending { color: $warning-color; background-color: #fff3e0; }
+          }
+        }
+      }
+
+      // 合作项目:名称/周期/描述/状态的网格分区,突出名称与状态
+      .projects-list {
+        .project-item {
+          display: grid;
+          grid-template-columns: 1fr auto;
+          grid-template-areas:
+            'name status'
+            'period status'
+            'desc desc';
+          row-gap: 4px;
+          column-gap: 12px;
+
+          .project-name { grid-area: name; font-size: 15px; font-weight: 600; color: $text-primary; }
+          .project-period { grid-area: period; font-size: 12px; color: $text-tertiary; font-variant-numeric: tabular-nums; }
+          .project-description { grid-area: desc; font-size: 14px; color: $text-secondary; line-height: 1.6; }
+          .project-status { grid-area: status; align-self: start; justify-self: end; font-size: 12px; font-weight: 600; padding: 2px 8px; border-radius: 10px; background: #f2f4f7; color: $text-secondary; }
+        }
+      }
+
+      // 历史反馈:日期等数值统一用等宽数字,正文稍加对比度
+      .feedback-list {
+        .feedback-item {
+          .feedback-date { font-variant-numeric: tabular-nums; letter-spacing: .2px; color: $text-light; }
+          .feedback-content { color: $text-primary; line-height: 1.7; }
+          .rating-number { font-variant-numeric: tabular-nums; }
+        }
+      }
+    }
+  }
+}
+
+// 响应式:在移动端收敛为纵向信息流
+@media (max-width: 768px) {
+  .project-detail-container {
+    .historical-records-card {
+      .record-section {
+        .consultation-list .consultation-item {
+          grid-template-columns: 1fr; // 纵向排列:内容 -> 状态 -> 日期
+          .consultation-status { justify-self: start; margin-top: 4px; }
+          .consultation-date { margin-top: 2px; }
+        }
+        .projects-list .project-item {
+          grid-template-columns: 1fr; grid-template-areas: 'name' 'status' 'period' 'desc';
+          .project-status { justify-self: start; margin: 2px 0 6px; }
+        }
+      }
+    }
+  }
 }

+ 1 - 1
src/app/pages/designer/dashboard/skill-radar/skill-radar.component.html

@@ -9,7 +9,7 @@
   
   <div class="skill-radar-content">
     <div class="radar-chart-wrapper">
-      <canvas #radarChart></canvas>
+      <div #radarChart style="width: 100%; height: 400px;"></div>
     </div>
     
     <div class="skill-legend">

+ 73 - 65
src/app/pages/designer/dashboard/skill-radar/skill-radar.component.ts

@@ -1,6 +1,6 @@
 import { Component, OnInit, AfterViewInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
 import { CommonModule } from '@angular/common';
-import { Chart } from 'chart.js';
+import * as echarts from 'echarts';
 
 interface SkillData {
   label: string;
@@ -22,8 +22,8 @@ interface SkillAnalysisItem {
   styleUrl: './skill-radar.component.scss'
 })
 export class SkillRadarComponent implements OnInit, AfterViewInit, OnDestroy {
-  @ViewChild('radarChart') radarChartRef!: ElementRef<HTMLCanvasElement>;
-  private radarChart: Chart | null = null;
+  @ViewChild('radarChart') radarChartRef!: ElementRef<HTMLDivElement>;
+  private radarChart: any = null;
   
   viewMode: 'self' | 'comparison' = 'self';
   
@@ -74,20 +74,11 @@ export class SkillRadarComponent implements OnInit, AfterViewInit, OnDestroy {
 
   // 初始化雷达图
   private initRadarChart(): void {
-    const ctx = this.radarChartRef.nativeElement.getContext('2d');
-    if (!ctx) return;
-
-    const labels = this.skillDimensions.map(dim => dim.label);
-    const data = this.prepareChartData();
-
-    this.radarChart = new Chart(ctx, {
-      type: 'radar',
-      data: {
-        labels: labels,
-        datasets: data
-      },
-      options: this.getChartOptions()
-    });
+    const chartDom = this.radarChartRef.nativeElement;
+    this.radarChart = echarts.init(chartDom);
+    
+    const option = this.getEChartsOption();
+    this.radarChart.setOption(option);
 
     // 窗口调整大小时重绘图表
     window.addEventListener('resize', () => {
@@ -97,75 +88,92 @@ export class SkillRadarComponent implements OnInit, AfterViewInit, OnDestroy {
     });
   }
 
-  // 准备图表数据
-  private prepareChartData() {
-    const datasets = [
+  // 获取ECharts配置选项
+  private getEChartsOption() {
+    const indicators = this.skillDimensions.map(dim => ({
+      name: dim.label,
+      max: 100
+    }));
+
+    const series = [
       {
-        label: '个人能力',
-        data: this.skillDimensions.map(dim => dim.current),
-        backgroundColor: 'rgba(255, 99, 132, 0.2)',
-        borderColor: 'rgba(255, 99, 132, 1)',
-        pointBackgroundColor: 'rgba(255, 99, 132, 1)',
-        pointBorderColor: '#fff',
-        pointHoverBackgroundColor: '#fff',
-        pointHoverBorderColor: 'rgba(255, 99, 132, 1)'
+        name: '个人能力',
+        type: 'radar',
+        data: [
+          {
+            value: this.skillDimensions.map(dim => dim.current),
+            name: '个人能力',
+            itemStyle: {
+              color: '#ff6384'
+            },
+            areaStyle: {
+              color: 'rgba(255, 99, 132, 0.2)'
+            }
+          }
+        ]
       }
     ];
 
     if (this.viewMode === 'comparison') {
-      datasets.push({
-        label: '团队平均',
-        data: this.skillDimensions.map(dim => dim.teamAvg),
-        backgroundColor: 'rgba(54, 162, 235, 0.2)',
-        borderColor: 'rgba(54, 162, 235, 1)',
-        pointBackgroundColor: 'rgba(54, 162, 235, 1)',
-        pointBorderColor: '#fff',
-        pointHoverBackgroundColor: '#fff',
-        pointHoverBorderColor: 'rgba(54, 162, 235, 1)'
+      series[0].data.push({
+        value: this.skillDimensions.map(dim => dim.teamAvg),
+        name: '团队平均',
+        itemStyle: {
+          color: '#36a2eb'
+        },
+        areaStyle: {
+          color: 'rgba(54, 162, 235, 0.2)'
+        }
       });
     }
 
-    return datasets;
-  }
-
-  // 获取图表配置
-  private getChartOptions() {
     return {
-      scales: {
-        r: {
-          angleLines: {
-            display: true,
-            color: 'rgba(200, 200, 200, 0.3)'
-          },
-          suggestedMin: 0,
-          suggestedMax: 100,
-          ticks: {
-            stepSize: 20,
-            backdropColor: 'transparent'
-          }
+      title: {
+        text: '能力雷达图',
+        left: 'center',
+        textStyle: {
+          fontSize: 16,
+          color: '#333'
         }
       },
-      plugins: {
-        legend: {
-          display: false
+      legend: {
+        data: this.viewMode === 'comparison' ? ['个人能力', '团队平均'] : ['个人能力'],
+        bottom: 10
+      },
+      radar: {
+        indicator: indicators,
+        radius: '60%',
+        axisLine: {
+          lineStyle: {
+            color: 'rgba(200, 200, 200, 0.3)'
+          }
         },
-        tooltip: {
-          callbacks: {
-            label: function(context: any) {
-              return `${context.dataset.label}: ${context.parsed.r}/100`;
-            }
+        splitLine: {
+          lineStyle: {
+            color: 'rgba(200, 200, 200, 0.3)'
           }
+        },
+        splitArea: {
+          show: false
         }
       },
-      maintainAspectRatio: false
+      series: series,
+      tooltip: {
+        trigger: 'item',
+        formatter: function(params: any) {
+          return `${params.seriesName}<br/>${params.name}: ${params.value}`;
+        }
+      }
     };
   }
 
+
+
   // 更新雷达图
   private updateRadarChart(): void {
     if (this.radarChart) {
-      this.radarChart.data.datasets = this.prepareChartData();
-      this.radarChart.update();
+      const option = this.getEChartsOption();
+      this.radarChart.setOption(option);
     }
   }
 

+ 4 - 0
src/app/pages/finance/reports/reports.html

@@ -180,6 +180,10 @@
         <h3>数据可视化</h3>
       </div>
       
+      <div class="role-warning" *ngIf="userRole() !== 'teamLead'">
+        出于权限控制,图表数值已隐藏;如需查看完整数据,请使用组长账号。
+      </div>
+      
       <!-- ECharts 图表容器 -->
       <div id="echarts-container" #chartContainer style="width: 100%; height: 400px;"></div>
       

+ 10 - 0
src/app/pages/finance/reports/reports.scss

@@ -289,6 +289,16 @@
   border-radius: 8px;
   padding: 20px;
   border: 1px solid #e9ecef;
+
+  .role-warning {
+    margin: 8px 0 12px;
+    padding: 10px 12px;
+    background: #fff7e6;
+    border: 1px solid #ffe7ba;
+    color: #ad6800;
+    border-radius: 6px;
+    font-size: 13px;
+  }
 }
 
 .chart-header {

+ 19 - 7
src/app/pages/finance/reports/reports.ts

@@ -1,9 +1,9 @@
-import { CommonModule } from '@angular/common';
+import { CommonModule, NgIf } from '@angular/common';
 import { FormsModule } from '@angular/forms';
 import { RouterModule } from '@angular/router';
 import { Component, signal, ElementRef, ViewChild, OnInit, AfterViewInit, effect } from '@angular/core';
 import { AuthService } from '../../../services/auth.service';
-// 使用全局echarts变量,不导入模块
+import * as echarts from 'echarts';
 
 // 报表类型定义
 export type ReportType = 
@@ -50,7 +50,7 @@ export interface ReportData {
 
 @Component({
   selector: 'app-reports',
-  imports: [CommonModule, FormsModule, RouterModule],
+  imports: [CommonModule, NgIf, FormsModule, RouterModule],
   templateUrl: './reports.html',
   styleUrl: './reports.scss',
   standalone: true
@@ -131,10 +131,17 @@ export class Reports implements OnInit, AfterViewInit {
   constructor(private authService: AuthService) {
     // 创建effect监听报表数据变化
     effect(() => {
-      // 访问filteredReportData以确保它被计算
-      this.filteredReportData;
+      // 访问并保存 filteredReportData,触发依赖跟踪
+      const data = this.filteredReportData;
+
+      // 当报表数据生成后,模板中的 *ngIf 渲染出图表容器,此时再初始化图表
+      if (!this.chartInitialized && data && this.chartContainer?.nativeElement) {
+        // 使用微任务/宏任务确保DOM已完成渲染
+        setTimeout(() => this.initChart());
+      }
       
-      if (this.filteredReportData && this.chartInitialized) {
+      // 数据变化且图表已初始化时,更新图表
+      if (data && this.chartInitialized) {
         this.updateChart();
       }
     });
@@ -154,7 +161,7 @@ export class Reports implements OnInit, AfterViewInit {
 
   // 初始化echarts图表
   private initChart(): void {
-    if (this.chartContainer && this.chartContainer.nativeElement) {
+    if (this.chartContainer && this.chartContainer.nativeElement && !this.chartInitialized) {
       this.chartInstance = echarts.init(this.chartContainer.nativeElement);
       this.chartInitialized = true;
       
@@ -162,6 +169,11 @@ export class Reports implements OnInit, AfterViewInit {
       window.addEventListener('resize', () => {
         this.chartInstance?.resize();
       });
+
+      // 初始化完成后,如已有数据则立即渲染一次
+      if (this.filteredReportData) {
+        this.updateChart();
+      }
     }
   }
 

+ 254 - 66
src/app/pages/team-leader/quality-management/quality-management.html

@@ -1,24 +1,135 @@
-<header>
-  <h1>质量管理</h1>
+<header class="quality-management-header">
+  <div class="header-content">
+    <div class="header-left">
+      <div class="header-icon">
+        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+          <path d="M9 12L11 14L15 10M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+        </svg>
+      </div>
+      <div class="header-text">
+        <h1>质量管理</h1>
+        <p class="header-subtitle">确保项目质量标准,提升团队专业能力</p>
+      </div>
+    </div>
+    <div class="header-actions">
+      <button class="btn-secondary header-btn">
+        <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+          <path d="M14 2L8 8L2 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+        </svg>
+        导出报告
+      </button>
+      <button class="btn-primary header-btn">
+        <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+          <path d="M8 2V14M2 8H14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+        </svg>
+        新建任务
+      </button>
+    </div>
+  </div>
+  
+  <!-- 统计卡片 -->
+  <div class="stats-cards">
+    <div class="stat-card">
+      <div class="stat-icon pending">
+        <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+          <circle cx="10" cy="10" r="8" stroke="currentColor" stroke-width="2"/>
+          <path d="M10 6V10L13 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+        </svg>
+      </div>
+      <div class="stat-content">
+        <div class="stat-number">12</div>
+        <div class="stat-label">待处理任务</div>
+      </div>
+    </div>
+    
+    <div class="stat-card">
+      <div class="stat-icon processing">
+        <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+          <path d="M10 2V6M10 14V18M18 10H14M6 10H2M15.5 4.5L12.5 7.5M7.5 12.5L4.5 15.5M15.5 15.5L12.5 12.5M7.5 7.5L4.5 4.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+        </svg>
+      </div>
+      <div class="stat-content">
+        <div class="stat-number">8</div>
+        <div class="stat-label">处理中</div>
+      </div>
+    </div>
+    
+    <div class="stat-card">
+      <div class="stat-icon completed">
+        <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+          <path d="M6 10L8.5 12.5L14 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+          <circle cx="10" cy="10" r="8" stroke="currentColor" stroke-width="2"/>
+        </svg>
+      </div>
+      <div class="stat-content">
+        <div class="stat-number">24</div>
+        <div class="stat-label">已完成</div>
+      </div>
+    </div>
+    
+    <div class="stat-card">
+      <div class="stat-icon quality">
+        <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+          <path d="M10 2L12.5 7L18 7.5L14 11L15 17L10 14.5L5 17L6 11L2 7.5L7.5 7L10 2Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
+        </svg>
+      </div>
+      <div class="stat-content">
+        <div class="stat-number">96%</div>
+        <div class="stat-label">质量达标率</div>
+      </div>
+    </div>
+  </div>
 </header>
 
 <main class="quality-management-main">
   <!-- 质量验收标准(SOP落地) -->
   <section class="sop-section">
     <div class="section-header">
-      <h2>质量验收标准 (SOP)</h2>
-      <button (click)="exportSOP()" class="btn-export">导出 PDF</button>
+      <div class="section-title">
+        <div class="section-icon">
+          <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+            <path d="M9 12L11 14L15 10M19 10C19 14.9706 14.9706 19 10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+          </svg>
+        </div>
+        <div>
+          <h2>质量验收标准 (SOP)</h2>
+          <p class="section-description">确保每个阶段的交付质量符合标准要求</p>
+        </div>
+      </div>
+      <button (click)="exportSOP()" class="btn-export">
+        <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+          <path d="M14 10V12.6667C14 13.0203 13.8595 13.3594 13.6095 13.6095C13.3594 13.8595 13.0203 14 12.6667 14H3.33333C2.97971 14 2.64057 13.8595 2.39052 13.6095C2.14048 13.3594 2 13.0203 2 12.6667V10M11.3333 6.66667L8 3.33333M8 3.33333L4.66667 6.66667M8 3.33333V10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+        </svg>
+        导出 PDF
+      </button>
     </div>
     
     <div class="sop-tabs">
       <div class="tab-buttons">
-          <button (click)="activeTab = 'modeling'" [class.active]="activeTab === 'modeling'">
+          <button (click)="activeTab = 'modeling'" [class.active]="activeTab === 'modeling'" class="tab-button">
+            <div class="tab-icon">
+              <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+                <path d="M2 2L16 2V16L2 16V2Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+                <path d="M6 6L12 6M6 9L12 9M6 12L9 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+              </svg>
+            </div>
             <span>建模阶段</span>
           </button>
-          <button (click)="activeTab = 'rendering'" [class.active]="activeTab === 'rendering'">
+          <button (click)="activeTab = 'rendering'" [class.active]="activeTab === 'rendering'" class="tab-button">
+            <div class="tab-icon">
+              <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+                <path d="M9 1L11.5 6L17 6.5L13 10L14 16L9 13.5L4 16L5 10L1 6.5L6.5 6L9 1Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
+              </svg>
+            </div>
             <span>渲染阶段</span>
           </button>
-          <button (click)="activeTab = 'postProduction'" [class.active]="activeTab === 'postProduction'">
+          <button (click)="activeTab = 'postProduction'" [class.active]="activeTab === 'postProduction'" class="tab-button">
+            <div class="tab-icon">
+              <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+                <path d="M9 2V9L13 13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+                <circle cx="9" cy="9" r="7" stroke="currentColor" stroke-width="1.5"/>
+              </svg>
+            </div>
             <span>后期阶段</span>
           </button>
         </div>
@@ -188,12 +299,39 @@
     </div>
   </section>
   
-  <!-- 效果图质量整改跟踪 -->
+  <!-- 质量整改跟踪 -->
   <section class="rectification-section">
     <div class="section-header">
-      <h2>质量整改跟踪</h2>
-      <div class="search-filter">
-        <input type="text" placeholder="搜索项目名称..." [(ngModel)]="searchTerm">
+      <div class="section-title">
+        <div class="title-icon">
+          <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+            <path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+          </svg>
+        </div>
+        <div class="title-content">
+          <h2>质量整改跟踪</h2>
+          <p>实时监控项目质量问题的整改进度和完成情况</p>
+        </div>
+      </div>
+      <div class="section-actions">
+        <button class="export-btn">
+          <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+            <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4m4-5l5-5 5 5m-5-5v12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+          </svg>
+          导出报告
+        </button>
+      </div>
+    </div>
+    
+    <div class="search-filter-modern">
+      <div class="search-input-wrapper">
+        <svg class="search-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+          <circle cx="11" cy="11" r="8" stroke="currentColor" stroke-width="2"/>
+          <path d="m21 21-4.35-4.35" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+        </svg>
+        <input type="text" placeholder="搜索项目名称或负责人..." [(ngModel)]="searchTerm">
+      </div>
+      <div class="filter-dropdown">
         <select [(ngModel)]="filterStatus">
           <option value="all">全部状态</option>
           <option value="pending">待处理</option>
@@ -201,15 +339,31 @@
           <option value="review">待审核</option>
           <option value="completed">已完成</option>
         </select>
+        <svg class="dropdown-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+          <path d="m6 9 6 6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+        </svg>
       </div>
     </div>
     
-    <div class="rectification-list">
+    <div class="rectification-grid">
       @for (task of filteredTasks; track task.id) {
-      <div class="rectification-item" [class.priority-high]="task.priority === 'high'" [class.priority-medium]="task.priority === 'medium'" [class.priority-low]="task.priority === 'low'">
-        <div class="rectification-header">
-          <h4>{{ task.projectName }}</h4>
-          <span [class]="'rectification-status status-' + task.status">{{ getStatusText(task.status) }}</span>
+      <div class="rectification-card" [class.priority-high]="task.priority === 'high'" [class.priority-medium]="task.priority === 'medium'" [class.priority-low]="task.priority === 'low'">
+        <div class="card-header">
+          <div class="project-info">
+            <div class="project-avatar">
+              <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+                <path d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M9 1v6m6-6v6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+              </svg>
+            </div>
+            <div class="project-details">
+              <h4>{{ task.projectName }}</h4>
+              <p class="project-designer">{{ task.designerName }}</p>
+            </div>
+          </div>
+          <div class="status-badge" [class]="'status-' + task.status">
+            <div class="status-indicator"></div>
+            <span>{{ getStatusText(task.status) }}</span>
+          </div>
         </div>
         
         <div class="rectification-details">
@@ -269,41 +423,70 @@
   <!-- 能力提升工具集成 -->
   <section class="training-section">
     <div class="section-header">
-      <h2>能力提升工具</h2>
-      <p class="section-description">通过视频教程和实践作业提升团队技能,确保SOP标准执行</p>
+      <div class="section-title">
+        <div class="title-icon">
+          <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+            <path d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+          </svg>
+        </div>
+        <div class="title-content">
+          <h2>能力提升工具</h2>
+          <p>通过视频教程和实践作业提升团队技能,确保SOP标准执行</p>
+        </div>
+      </div>
+      <div class="section-actions">
+        <button class="progress-btn">
+          <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+            <path d="M9 19c-5 0-8-3-8-8s3-8 8-8 8 3 8 8-3 8-8 8z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+            <path d="M9 9h.01M9 13h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+          </svg>
+          学习进度
+        </button>
+      </div>
     </div>
     
     <div class="training-content">
       <!-- 视频教程区域 -->
       <div class="video-tutorials">
-        <div class="section-subheader">
-          <h3>视频教程</h3>
-          <button class="btn-view-all">查看全部</button>
+        <div class="subsection-header">
+          <div class="subsection-title">
+            <div class="subsection-icon">
+              <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+                <path d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+              </svg>
+            </div>
+            <h3>视频教程</h3>
+          </div>
+          <button class="view-all-btn">
+            <span>查看全部</span>
+            <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <path d="m9 18 6-6-6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+            </svg>
+          </button>
         </div>
         <div class="video-grid">
           @for (video of videoTutorials; track video.id) {
-          <div class="video-card" (mouseenter)="video.isHovered = true" (mouseleave)="video.isHovered = false">
+          <div class="video-card" (click)="playVideo(video)" (mouseenter)="video.isHovered = true" (mouseleave)="video.isHovered = false">
             <div class="video-thumbnail-container">
               <div class="video-thumbnail">
-                <img [src]="video.thumbnailUrl" alt="视频缩略图" class="thumbnail-img">
+                <img [src]="video.thumbnailUrl" [alt]="video.title" class="thumbnail-img">
                 <div class="video-duration">{{ video.duration }}</div>
-                <div class="play-overlay" [class.visible]="video.isHovered">
-                  <button (click)="playVideo(video.id)" class="play-button">
-                    <svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
-                      <circle cx="20" cy="20" r="20" fill="rgba(0,0,0,0.6)"/>
-                      <path d="M15 13L28 20L15 27V13Z" fill="white"/>
+                <div class="play-overlay" [class.visible]="true">
+                  <button class="play-button">
+                    <svg viewBox="0 0 24 24">
+                      <path d="M8 5v14l11-7z"/>
                     </svg>
                   </button>
                 </div>
               </div>
             </div>
             <div class="video-info">
-              <div class="category-badge">{{ video.category }}</div>
+              <span class="category-badge">{{ video.category }}</span>
               <h4 class="video-title">{{ video.title }}</h4>
               <p class="video-description">{{ video.description }}</p>
               <div class="video-meta">
                 <span class="views">{{ video.views }} 次观看</span>
-                <span class="date">2天前</span>
+                <span class="date">{{ video.date }}</span>
               </div>
             </div>
           </div>
@@ -313,53 +496,58 @@
       
       <!-- 实践作业区域 -->
       <div class="practice-assignments">
-        <div class="section-subheader">
-          <h3>实践作业</h3>
-          <div class="assignment-stats">
-            <span class="stat">待评审: 2</span>
-            <span class="stat">进行中: 3</span>
+        <div class="subsection-header">
+          <div class="subsection-title">
+            <div class="subsection-icon">
+              <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+                <path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8l-6-6z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+                <path d="M14 2v6h6M16 13H8M16 17H8M10 9H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+              </svg>
+            </div>
+            <h3>实践作业</h3>
           </div>
+          <button class="view-all-btn">
+            <span>查看全部</span>
+            <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <path d="m9 18 6-6-6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+            </svg>
+          </button>
         </div>
-        <div class="assignment-list">
+        <div class="assignment-grid">
           @for (assignment of practiceAssignments; track assignment.id) {
-          <div class="assignment-card">
+          <div class="assignment-card" (click)="openAssignment(assignment)">
             <div class="assignment-header">
-              <div class="assignment-title-section">
-                <h4>{{ assignment.title }}</h4>
-                <div class="assignment-badges">
-                  <span class="badge related-sop">关联SOP: {{ assignment.relatedSOP }}</span>
-                </div>
+              <div class="assignment-icon">
+                <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+                  <path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8l-6-6z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+                </svg>
               </div>
-              <span [class]="'status-badge status-' + assignment.status">{{ getAssignmentStatusText(assignment.status) }}</span>
-            </div>
-            <div class="assignment-details">
-              <p class="description">{{ assignment.description }}</p>
               <div class="assignment-meta">
-                <div class="meta-item">
-                  <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-                    <path d="M8 2V3M8 13V14M1 8H2M14 8H15M12.5 3.5L13.5 2.5M3.5 13.5L2.5 12.5M12.5 12.5L13.5 13.5M3.5 3.5L2.5 2.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-                    <circle cx="8" cy="8" r="5" stroke="currentColor" stroke-width="2"/>
-                  </svg>
-                  <span>截止日期: {{ assignment.deadline | date:'yyyy-MM-dd' }}</span>
-                </div>
-                @if (assignment.score !== null) {
-                <div class="meta-item score-item">
-                  <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-                    <path d="M8 1L10.5 5.5L16 6L12 9L13 14L8 12L3 14L4 9L0 6L5.5 5.5L8 1Z" stroke="currentColor" stroke-width="2" fill="none"/>
-                  </svg>
-                  <span>得分: {{ assignment.score }}/100</span>
+                <span class="difficulty-badge" [class]="'difficulty-' + assignment.difficulty">{{ assignment.difficulty }}</span>
+                <span class="assignment-type">{{ assignment.type }}</span>
+              </div>
+            </div>
+            <div class="assignment-content">
+              <h4 class="assignment-title">{{ assignment.title }}</h4>
+              <p class="assignment-description">{{ assignment.description }}</p>
+              <div class="assignment-progress">
+                <div class="progress-bar">
+                  <div class="progress-fill" [style.width.%]="assignment.progress"></div>
                 </div>
-                }
+                <span class="progress-text">{{ assignment.progress }}%</span>
               </div>
             </div>
-            <div class="assignment-actions">
-              <button (click)="reviewAssignment(assignment.id)" class="btn-review btn-primary">
-                <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-                  <path d="M8 1L10.5 5.5L16 6L12 9L13 14L8 12L3 14L4 9L0 6L5.5 5.5L8 1Z" stroke="white" stroke-width="2" fill="none"/>
+            <div class="assignment-footer">
+              <div class="assignment-stats">
+                <span class="deadline">{{ assignment.deadline | date:'yyyy-MM-dd' }}</span>
+                <span class="points">{{ assignment.points }} 积分</span>
+              </div>
+              <button class="start-btn" [class.completed]="assignment.progress === 100">
+                <span>{{ assignment.progress === 100 ? '已完成' : '开始练习' }}</span>
+                <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+                  <path d="M8 5v14l11-7z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
                 </svg>
-                评审作业
               </button>
-              <button class="btn-secondary">下载资料</button>
             </div>
           </div>
           }

Diff do ficheiro suprimidas por serem muito extensas
+ 872 - 196
src/app/pages/team-leader/quality-management/quality-management.scss


+ 36 - 13
src/app/pages/team-leader/quality-management/quality-management.ts

@@ -28,6 +28,7 @@ interface VideoTutorial {
   views: number;
   relatedSOP: string;
   isHovered: boolean;
+  date: string;
 }
 
 // 定义实践作业接口
@@ -40,6 +41,10 @@ interface PracticeAssignment {
   status: 'pending' | 'submitted' | 'reviewed';
   score: number | null;
   designerName: string;
+  points: number;
+  progress: number;
+  difficulty: 'easy' | 'medium' | 'hard';
+  type: string;
 }
 
 @Component({
@@ -138,7 +143,8 @@ export class QualityManagementComponent implements OnInit {
         category: '渲染技巧',
         views: 1256,
         relatedSOP: '渲染阶段',
-        isHovered: false
+        isHovered: false,
+        date: '2023-12-01'
       },
       {
         id: 'video-2',
@@ -149,7 +155,8 @@ export class QualityManagementComponent implements OnInit {
         category: '建模技巧',
         views: 892,
         relatedSOP: '建模阶段',
-        isHovered: false
+        isHovered: false,
+        date: '2023-11-28'
       },
       {
         id: 'video-3',
@@ -160,7 +167,8 @@ export class QualityManagementComponent implements OnInit {
         category: '后期处理',
         views: 1053,
         relatedSOP: '后期阶段',
-        isHovered: false
+        isHovered: false,
+        date: '2023-12-05'
       }
     ];
     
@@ -174,7 +182,11 @@ export class QualityManagementComponent implements OnInit {
         deadline: new Date('2023-12-25'),
         status: 'submitted',
         score: null,
-        designerName: '张三'
+        designerName: '张三',
+        points: 85,
+        progress: 100,
+        difficulty: 'medium',
+        type: '渲染练习'
       },
       {
         id: 'assignment-2',
@@ -184,7 +196,11 @@ export class QualityManagementComponent implements OnInit {
         deadline: new Date('2024-01-05'),
         status: 'pending',
         score: null,
-        designerName: '李四'
+        designerName: '李四',
+        points: 75,
+        progress: 30,
+        difficulty: 'hard',
+        type: '建模练习'
       },
       {
         id: 'assignment-3',
@@ -194,7 +210,11 @@ export class QualityManagementComponent implements OnInit {
         deadline: new Date('2023-12-30'),
         status: 'reviewed',
         score: 92,
-        designerName: '王五'
+        designerName: '王五',
+        points: 90,
+        progress: 100,
+        difficulty: 'easy',
+        type: '后期处理'
       }
     ];
   }
@@ -303,13 +323,10 @@ export class QualityManagementComponent implements OnInit {
   }
   
   // 播放视频
-  playVideo(videoId: string): void {
-    const video = this.videoTutorials.find(v => v.id === videoId);
-    if (video) {
-      // 模拟播放视频
-      alert(`正在播放视频: ${video.title}`);
-      // 实际项目中,这里应该打开视频播放器或跳转到视频播放页面
-    }
+  playVideo(video: VideoTutorial): void {
+    // 模拟播放视频
+    alert(`正在播放视频: ${video.title}`);
+    // 实际项目中,这里应该打开视频播放器或跳转到视频播放页面
   }
   
   // 评审作业
@@ -349,4 +366,10 @@ export class QualityManagementComponent implements OnInit {
   navigateToProjectReview(): void {
     this.router.navigate(['/team-leader/project-review']);
   }
+
+  // 打开作业详情
+  openAssignment(assignment: PracticeAssignment): void {
+    console.log('打开作业详情:', assignment);
+    // 这里可以实现打开作业详情的逻辑,比如导航到详情页面或打开模态框
+  }
 }

+ 2 - 6
src/index.html

@@ -10,12 +10,8 @@
     <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
     
     <!-- 使用国内CDN引入echarts -->
-    <script src="https://unpkg.com/echarts@6.0.0/dist/echarts.min.js"></script>
-    <!-- 声明全局变量告知Angular使用外部的echarts -->
-    <script>
-      // 全局变量,供Angular应用使用
-      window.echarts = echarts;
-    </script>
+    <!-- ECharts will be imported in components as needed -->
+    <!-- ECharts will be imported locally in components -->
   </head>
 <body>
   <app-root></app-root>

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff