|
|
@@ -17,52 +17,15 @@
|
|
|
|
|
|
<header class="dashboard-header">
|
|
|
<!-- 核心数据指标卡片(扩展为6个) -->
|
|
|
- <div class="dashboard-metrics">
|
|
|
- <div class="metric-card" (click)="filterByStatus('overdue')">
|
|
|
- <div class="metric-icon warning">⚠️</div>
|
|
|
- <div class="metric-content">
|
|
|
- <div class="metric-count">{{ overdueProjects.length }}</div>
|
|
|
- <div class="metric-label">已延期项目</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div class="metric-card" (click)="filterByStatus('dueSoon')">
|
|
|
- <div class="metric-icon info">⏳</div>
|
|
|
- <div class="metric-content">
|
|
|
- <div class="metric-count">{{ dueSoonProjects.length }}</div>
|
|
|
- <div class="metric-label">临期项目(3天内)</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div class="metric-card" (click)="filterByStatus('pendingApproval')">
|
|
|
- <div class="metric-icon info">📋</div>
|
|
|
- <div class="metric-content">
|
|
|
- <div class="metric-count">{{ pendingApprovalProjects.length }}</div>
|
|
|
- <div class="metric-label">待组长确认项目</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div class="metric-card" (click)="filterByStatus('pendingAssignment')">
|
|
|
- <div class="metric-icon primary">🎯</div>
|
|
|
- <div class="metric-content">
|
|
|
- <div class="metric-count">{{ pendingAssignmentProjects.length }}</div>
|
|
|
- <div class="metric-label">待分配方案项目</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <!-- 新增:超负荷设计师数量 -->
|
|
|
- <div class="metric-card">
|
|
|
- <div class="metric-icon danger">🔥</div>
|
|
|
- <div class="metric-content">
|
|
|
- <div class="metric-count">{{ overloadedDesignersCount }}</div>
|
|
|
- <div class="metric-label">超负荷设计师</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <!-- 新增:平均负载率 -->
|
|
|
- <div class="metric-card">
|
|
|
- <div class="metric-icon success">📊</div>
|
|
|
- <div class="metric-content">
|
|
|
- <div class="metric-count">{{ averageWorkloadRate.toFixed(0) }}%</div>
|
|
|
- <div class="metric-label">平均负载率</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <app-dashboard-metrics
|
|
|
+ [overdueCount]="overdueProjects.length"
|
|
|
+ [dueSoonCount]="dueSoonProjects.length"
|
|
|
+ [pendingApprovalCount]="pendingApprovalProjects.length"
|
|
|
+ [pendingAssignmentCount]="pendingAssignmentProjects.length"
|
|
|
+ [overloadedDesignersCount]="overloadedDesignersCount"
|
|
|
+ [averageWorkloadRate]="averageWorkloadRate"
|
|
|
+ (filterStatus)="filterByStatus($event)">
|
|
|
+ </app-dashboard-metrics>
|
|
|
</header>
|
|
|
|
|
|
<main class="dashboard-main">
|
|
|
@@ -78,27 +41,14 @@
|
|
|
</div>
|
|
|
|
|
|
<!-- 工作量负载概览 -->
|
|
|
- <div class="workload-gantt-card">
|
|
|
- <div class="gantt-header">
|
|
|
- <h3>工作量负载概览</h3>
|
|
|
- <p class="gantt-subtitle">设计师每日工作状态一目了然</p>
|
|
|
- <div class="gantt-controls">
|
|
|
- <div class="scale-switch">
|
|
|
- <button [class.active]="workloadGanttScale === 'week'" (click)="setWorkloadGanttScale('week')">周视图</button>
|
|
|
- <button [class.active]="workloadGanttScale === 'month'" (click)="setWorkloadGanttScale('month')">月视图</button>
|
|
|
- </div>
|
|
|
- <div class="legend">
|
|
|
- <span class="legend-item"><span class="dot idle"></span>空闲</span>
|
|
|
- <span class="legend-item"><span class="dot busy"></span>忙碌</span>
|
|
|
- <span class="legend-item"><span class="dot overload"></span>超负荷</span>
|
|
|
- <span class="legend-item"><span class="dot leave"></span>请假</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div class="gantt-container" #workloadGanttContainer></div>
|
|
|
- </div>
|
|
|
+ <app-workload-gantt
|
|
|
+ [designerWorkloadMap]="designerWorkloadMap"
|
|
|
+ [realDesigners]="realDesigners"
|
|
|
+ [filteredProjects]="filteredProjects"
|
|
|
+ (employeeClick)="onEmployeeClick($event)">
|
|
|
+ </app-workload-gantt>
|
|
|
|
|
|
- <!-- 🆕 视图切换按钮(固定在此位置便于切换) -->
|
|
|
+ <!-- 视图切换按钮(固定在此位置便于切换) -->
|
|
|
<div class="view-toggle-bar">
|
|
|
<button class="btn-toggle-view" (click)="toggleView()">
|
|
|
<span class="toggle-icon">{{ showGanttView ? '📋' : '📊' }}</span>
|
|
|
@@ -116,1071 +66,77 @@
|
|
|
}
|
|
|
|
|
|
@if (!showGanttView) {
|
|
|
- <div class="section-filters">
|
|
|
- <div class="search-box">
|
|
|
- <input type="search" class="input-search" placeholder="搜索项目/设计师/风格关键词" [(ngModel)]="searchTerm" (input)="onSearchChange()" (focus)="onSearchFocus()" (blur)="onSearchBlur()" />
|
|
|
- @if (showSuggestions) {
|
|
|
- <div class="suggestion-panel">
|
|
|
- @if (searchSuggestions.length > 0) {
|
|
|
- <ul>
|
|
|
- @for (suggest of searchSuggestions; track suggest.id) {
|
|
|
- <li (mousedown)="selectSuggestion(suggest)">
|
|
|
- <div class="line-1">
|
|
|
- <span class="name">{{ suggest.name }}</span>
|
|
|
- <span class="badge" [class.vip]="suggest.memberType==='vip'">{{ suggest.memberType==='vip' ? 'VIP' : '普通' }}</span>
|
|
|
- <span class="urgency" [class]="'u-' + suggest.urgency">{{ getUrgencyLabel(suggest.urgency) }}</span>
|
|
|
- </div>
|
|
|
- <div class="line-2">
|
|
|
- <span class="designer">{{ suggest.designerName || '未分配' }}</span>
|
|
|
- <span class="deadline">{{ suggest.deadline | date:'MM-dd' }}</span>
|
|
|
- </div>
|
|
|
- </li>
|
|
|
- }
|
|
|
- </ul>
|
|
|
- } @else {
|
|
|
- <div class="empty">抱歉,没有检索到哦</div>
|
|
|
- }
|
|
|
- </div>
|
|
|
- }
|
|
|
- </div>
|
|
|
- <select (change)="filterProjects($event)" class="custom-select">
|
|
|
- <option value="all">全部项目</option>
|
|
|
- <option value="soft">软装项目</option>
|
|
|
- <option value="hard">硬装项目</option>
|
|
|
- </select>
|
|
|
- <select (change)="filterByUrgency($event)" class="custom-select">
|
|
|
- <option value="all">全部紧急程度</option>
|
|
|
- <option value="high">高</option>
|
|
|
- <option value="medium">中</option>
|
|
|
- <option value="low">低</option>
|
|
|
- </select>
|
|
|
- <select (change)="onStatusChange($event)" class="custom-select">
|
|
|
- <option value="all">全部状态</option>
|
|
|
- <option value="progress">进行中</option>
|
|
|
- <option value="completed">已完成</option>
|
|
|
- <option value="overdue">已延期</option>
|
|
|
- <option value="dueSoon">临期(3天内)</option>
|
|
|
- <option value="pendingApproval">待确认</option>
|
|
|
- <option value="pendingAssignment">待分配</option>
|
|
|
- </select>
|
|
|
- <select (change)="onDesignerChange($event)" class="custom-select">
|
|
|
- <option value="all">全部设计师</option>
|
|
|
- @for (d of designers; track d) {
|
|
|
- <option [value]="d">{{ d }}</option>
|
|
|
- }
|
|
|
- </select>
|
|
|
- <select (change)="onMemberTypeChange($event)" class="custom-select">
|
|
|
- <option value="all">全部会员</option>
|
|
|
- <option value="vip">VIP会员</option>
|
|
|
- <option value="normal">普通会员</option>
|
|
|
- </select>
|
|
|
- <!-- 新增:四大板块筛选 -->
|
|
|
- <select [(ngModel)]="selectedCorePhase" (change)="onCorePhaseChange($event)" class="custom-select">
|
|
|
- <option value="all">全部板块</option>
|
|
|
- @for (core of corePhases; track core.id) {
|
|
|
- <option [value]="core.id">{{ core.name }}</option>
|
|
|
- }
|
|
|
- </select>
|
|
|
- <!-- 支持数百项目的下拉筛选 -->
|
|
|
- <select [(ngModel)]="selectedProjectId" (change)="selectProject()" class="custom-select project-selector">
|
|
|
- <option value="">选择项目</option>
|
|
|
- @for (project of projects; track project.id) {
|
|
|
- <option [value]="project.id">{{ project.name }}</option>
|
|
|
- }
|
|
|
- </select>
|
|
|
- <!-- 新增:时间窗快捷筛选按钮组 -->
|
|
|
- <div class="time-window-buttons">
|
|
|
- <button [class.active]="selectedTimeWindow === 'all'" (click)="filterByTimeWindow('all')">全部</button>
|
|
|
- <button [class.active]="selectedTimeWindow === 'today'" (click)="filterByTimeWindow('today')">今天到期</button>
|
|
|
- <button [class.active]="selectedTimeWindow === 'threeDays'" (click)="filterByTimeWindow('threeDays')">3天内</button>
|
|
|
- <button [class.active]="selectedTimeWindow === 'sevenDays'" (click)="filterByTimeWindow('sevenDays')">7天内</button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <app-dashboard-filter-bar
|
|
|
+ [projects]="projects"
|
|
|
+ [designers]="designers"
|
|
|
+ [corePhases]="corePhases"
|
|
|
+ [(searchTerm)]="searchTerm"
|
|
|
+ [(selectedType)]="selectedType"
|
|
|
+ [(selectedUrgency)]="selectedUrgency"
|
|
|
+ [(selectedStatus)]="selectedStatus"
|
|
|
+ [(selectedDesigner)]="selectedDesigner"
|
|
|
+ [(selectedMemberType)]="selectedMemberType"
|
|
|
+ [(selectedCorePhase)]="selectedCorePhase"
|
|
|
+ [(selectedProjectId)]="selectedProjectId"
|
|
|
+ [(selectedTimeWindow)]="selectedTimeWindow"
|
|
|
+ (filterChange)="applyFilters()"
|
|
|
+ (viewProject)="viewProjectDetails($event)">
|
|
|
+ </app-dashboard-filter-bar>
|
|
|
|
|
|
- <!-- 项目看板 - 横向展开10个项目阶段 -->
|
|
|
- <div class="project-kanban">
|
|
|
- <!-- 新增:公共横向滚动容器,保证表头与表体同步滚动 -->
|
|
|
- <div class="kanban-scroll">
|
|
|
- <!-- 阶段标题 -->
|
|
|
- <div class="kanban-header">
|
|
|
- @for (core of corePhases; track core.id) {
|
|
|
- <div class="kanban-column-header">
|
|
|
- <h3>{{ core.name }}</h3>
|
|
|
- <span class="stage-count">{{ getProjectCountByCorePhase(core.id) }}</span>
|
|
|
- </div>
|
|
|
- }
|
|
|
- </div>
|
|
|
- <!-- 项目卡片 -->
|
|
|
- <div class="kanban-body">
|
|
|
- @for (core of corePhases; track core.id) {
|
|
|
- <div class="kanban-column">
|
|
|
- @for (project of getProjectsByCorePhase(core.id); track project.id) {
|
|
|
- <div class="project-card"
|
|
|
- (click)="viewProjectDetailsByPhase(project.id, core.id)"
|
|
|
- [class.overdue]="project.isOverdue"
|
|
|
- [class.high-urgency]="project.urgency === 'high'"
|
|
|
- [class.due-soon]="project.dueSoon && !project.isOverdue"
|
|
|
- [class.pending-approval]="isPendingApproval(project)">
|
|
|
- <!-- 待审批徽章 -->
|
|
|
- @if (isPendingApproval(project)) {
|
|
|
- <div class="approval-badge">
|
|
|
- <span class="badge-icon">📋</span>
|
|
|
- <span class="badge-text">待审批</span>
|
|
|
- </div>
|
|
|
- }
|
|
|
- <div class="project-card-header">
|
|
|
- <h4 (click)="viewProjectDetailsByPhase(project.id, core.id); $event.stopPropagation()" style="cursor: pointer;">{{ project.name }}</h4>
|
|
|
- <div class="right-badges">
|
|
|
- <span class="member-badge" [class.vip]="project.memberType === 'vip'">{{ project.memberType === 'vip' ? 'VIP' : '普通' }}</span>
|
|
|
- <span class="project-urgency" [class]="'urgency-' + project.urgency">{{ getUrgencyLabel(project.urgency) }}</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div class="project-card-content">
|
|
|
- <p>负责人: {{ project.designerName || '未分配' }}</p>
|
|
|
- <p class="deadline">{{ project.isOverdue ? '超期' + project.overdueDays + '天' : (project.dueSoon ? '临期: ' + (project.deadline | date:'MM-dd') : '截止: ' + (project.deadline | date:'MM-dd')) }}</p>
|
|
|
- </div>
|
|
|
- <div class="project-card-footer">
|
|
|
- <button (click)="viewProjectDetailsByPhase(project.id, core.id); $event.stopPropagation()" class="btn-view">查看详情</button>
|
|
|
- @if (project.currentStage === 'pendingAssignment') {
|
|
|
- <button (click)="openSmartMatch(project); $event.stopPropagation()" class="btn-smart">🤖 智能推荐</button>
|
|
|
- <button (click)="quickAssignProject(project.id); $event.stopPropagation()" class="btn-assign">手动分配</button>
|
|
|
- }
|
|
|
- <!-- 新增:质量评审快捷操作(保留,不影响四大板块分类) -->
|
|
|
- @if (project.currentStage === 'review' || project.currentStage === 'delivery') {
|
|
|
- <div class="inline-actions">
|
|
|
- <button class="btn-secondary" (click)="$event.stopPropagation(); reviewProjectQuality(project.id, 'excellent')">评为优秀</button>
|
|
|
- <button class="btn-secondary" (click)="$event.stopPropagation(); reviewProjectQuality(project.id, 'qualified')">评为合格</button>
|
|
|
- <button class="btn-secondary" (click)="$event.stopPropagation(); reviewProjectQuality(project.id, 'unqualified')">评为不合格</button>
|
|
|
- </div>
|
|
|
- }
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- }
|
|
|
- @if (getProjectsByCorePhase(core.id).length === 0) {
|
|
|
- <div class="empty-column">
|
|
|
- <span class="empty-icon">📦</span>
|
|
|
- <p>暂无项目</p>
|
|
|
- </div>
|
|
|
- }
|
|
|
- </div>
|
|
|
- }
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <!-- 项目看板组件 -->
|
|
|
+ <app-project-kanban
|
|
|
+ [corePhases]="corePhases"
|
|
|
+ [projects]="filteredProjects"
|
|
|
+ (viewProject)="viewProjectDetailsByPhase($event.projectId, $event.phaseId)"
|
|
|
+ (openSmartMatch)="openSmartMatch($event)"
|
|
|
+ (assignProject)="quickAssignProject($event)"
|
|
|
+ (reviewProject)="reviewProjectQuality($event.projectId, $event.rating)">
|
|
|
+ </app-project-kanban>
|
|
|
}
|
|
|
</section>
|
|
|
|
|
|
- <!-- 🆕 待办任务双栏布局(待办问题 + 紧急事件) -->
|
|
|
- <section class="todo-section todo-section-dual">
|
|
|
- <div class="section-header">
|
|
|
- <h2>待办事项</h2>
|
|
|
- <button
|
|
|
- class="btn-refresh"
|
|
|
- (click)="refreshTodoTasks()"
|
|
|
- [disabled]="loadingTodoTasks || loadingUrgentEvents"
|
|
|
- title="刷新待办事项">
|
|
|
- <svg viewBox="0 0 24 24" width="16" height="16" [class.rotating]="loadingTodoTasks || loadingUrgentEvents">
|
|
|
- <path fill="currentColor" d="M17.65 6.35A7.958 7.958 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>
|
|
|
- </svg>
|
|
|
- </button>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 🆕 双栏容器 -->
|
|
|
- <div class="todo-dual-columns">
|
|
|
- <!-- ========== 左栏:待办问题 ========== -->
|
|
|
- <div class="todo-column todo-column-issues">
|
|
|
- <div class="column-header">
|
|
|
- <h3>
|
|
|
- <svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
|
|
|
- <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
|
|
|
- </svg>
|
|
|
- 待办问题
|
|
|
- @if (todoTasksFromIssues.length > 0) {
|
|
|
- <span class="task-count">({{ todoTasksFromIssues.length }})</span>
|
|
|
- }
|
|
|
- </h3>
|
|
|
- <span class="column-subtitle">来自项目问题板块</span>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 加载状态 -->
|
|
|
- @if (loadingTodoTasks) {
|
|
|
- <div class="loading-state">
|
|
|
- <svg class="spinner" viewBox="0 0 50 50">
|
|
|
- <circle cx="25" cy="25" r="20" fill="none" stroke-width="4"></circle>
|
|
|
- </svg>
|
|
|
- <p>加载待办任务中...</p>
|
|
|
- </div>
|
|
|
- }
|
|
|
-
|
|
|
- <!-- 错误状态 -->
|
|
|
- @if (!loadingTodoTasks && todoTaskError) {
|
|
|
- <div class="error-state">
|
|
|
- <svg viewBox="0 0 24 24" width="48" height="48" fill="#ef4444">
|
|
|
- <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
|
|
|
- </svg>
|
|
|
- <p>{{ todoTaskError }}</p>
|
|
|
- <button class="btn-retry" (click)="refreshTodoTasks()">重试</button>
|
|
|
- </div>
|
|
|
- }
|
|
|
-
|
|
|
- <!-- 空状态 -->
|
|
|
- @if (!loadingTodoTasks && !todoTaskError && todoTasksFromIssues.length === 0) {
|
|
|
- <div class="empty-state">
|
|
|
- <svg viewBox="0 0 24 24" width="64" height="64" fill="#d1d5db">
|
|
|
- <path d="M19 3h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm2 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/>
|
|
|
- </svg>
|
|
|
- <p>暂无待办任务</p>
|
|
|
- <p class="hint">所有项目问题都已处理完毕 🎉</p>
|
|
|
- </div>
|
|
|
- }
|
|
|
-
|
|
|
- <!-- 待办任务列表 -->
|
|
|
- @if (!loadingTodoTasks && !todoTaskError && todoTasksFromIssues.length > 0) {
|
|
|
- <div class="todo-list-compact">
|
|
|
- @for (task of todoTasksFromIssues; track task.id) {
|
|
|
- <div class="todo-item-compact" [attr.data-priority]="task.priority">
|
|
|
- <!-- 左侧优先级色条 -->
|
|
|
- <div class="priority-indicator" [attr.data-priority]="task.priority"></div>
|
|
|
-
|
|
|
- <!-- 任务内容 -->
|
|
|
- <div class="task-content">
|
|
|
- <!-- 标题行 -->
|
|
|
- <div class="task-header">
|
|
|
- <span class="task-title">{{ task.title }}</span>
|
|
|
- <div class="task-badges">
|
|
|
- <span class="badge badge-priority" [attr.data-priority]="task.priority">
|
|
|
- {{ getPriorityConfig(task.priority).label }}
|
|
|
- </span>
|
|
|
- <span class="badge badge-type">{{ getIssueTypeLabel(task.type) }}</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 项目信息行 -->
|
|
|
- <div class="task-meta">
|
|
|
- <span class="project-info">
|
|
|
- <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
|
|
|
- <path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
|
|
|
- </svg>
|
|
|
- 项目: {{ task.projectName }}
|
|
|
- @if (task.relatedSpace) {
|
|
|
- | {{ task.relatedSpace }}
|
|
|
- }
|
|
|
- @if (task.relatedStage) {
|
|
|
- | {{ task.relatedStage }}
|
|
|
- }
|
|
|
- </span>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 底部信息行 -->
|
|
|
- <div class="task-footer">
|
|
|
- <span class="time-info" [title]="formatExactTime(task.createdAt)">
|
|
|
- <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
|
|
|
- <path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z"/>
|
|
|
- </svg>
|
|
|
- 创建于 {{ formatRelativeTime(task.createdAt) }}
|
|
|
- </span>
|
|
|
-
|
|
|
- <span class="assignee-info">
|
|
|
- <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
|
|
|
- <path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
|
|
|
- </svg>
|
|
|
- 指派给: {{ task.assigneeName }}
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 右侧操作按钮 -->
|
|
|
- <div class="task-actions">
|
|
|
- <button
|
|
|
- class="btn-action btn-view"
|
|
|
- (click)="navigateToIssue(task)"
|
|
|
- title="查看详情">
|
|
|
- <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
|
|
|
- <path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/>
|
|
|
- </svg>
|
|
|
- 查看详情
|
|
|
- </button>
|
|
|
- <button
|
|
|
- class="btn-action btn-mark-read"
|
|
|
- (click)="markAsRead(task)"
|
|
|
- title="标记已读">
|
|
|
- <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
|
|
|
- <path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/>
|
|
|
- </svg>
|
|
|
- 标记已读
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- }
|
|
|
- </div>
|
|
|
- }
|
|
|
- </div>
|
|
|
- <!-- ========== 左栏结束 ========== -->
|
|
|
-
|
|
|
- <!-- ========== 右栏:紧急事件 ========== -->
|
|
|
- <div class="todo-column todo-column-urgent">
|
|
|
- <div class="column-header">
|
|
|
- <h3>
|
|
|
- <svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
|
|
|
- <path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>
|
|
|
- </svg>
|
|
|
- 紧急事件
|
|
|
- @if (urgentEvents.length > 0) {
|
|
|
- <span class="task-count urgent">({{ urgentEvents.length }})</span>
|
|
|
- }
|
|
|
- </h3>
|
|
|
- <span class="column-subtitle">自动计算的截止事件</span>
|
|
|
- </div>
|
|
|
-
|
|
|
- @if (!loadingUrgentEvents && urgentEvents.length > 0) {
|
|
|
- <div class="tag-filter-bar">
|
|
|
- <button
|
|
|
- class="tag-button"
|
|
|
- [class.active]="urgentEventTagFilter === 'all'"
|
|
|
- (click)="filterUrgentEventsByTag('all')"
|
|
|
- title="显示所有紧急事件"
|
|
|
- >
|
|
|
- <span class="tag-icon">📋</span>
|
|
|
- <span class="tag-label">全部</span>
|
|
|
- <span class="tag-count">{{ urgentEvents.length }}</span>
|
|
|
- </button>
|
|
|
-
|
|
|
- <button
|
|
|
- class="tag-button"
|
|
|
- [class.active]="urgentEventTagFilter === 'customer'"
|
|
|
- (click)="filterUrgentEventsByTag('customer')"
|
|
|
- title="客户服务"
|
|
|
- >
|
|
|
- <span class="tag-icon">👥</span>
|
|
|
- <span class="tag-label">客户服务</span>
|
|
|
- <span class="tag-count">{{ getTagCount('customer') }}</span>
|
|
|
- </button>
|
|
|
-
|
|
|
- <button
|
|
|
- class="tag-button"
|
|
|
- [class.active]="urgentEventTagFilter === 'phase'"
|
|
|
- (click)="filterUrgentEventsByTag('phase')"
|
|
|
- title="工作阶段"
|
|
|
- >
|
|
|
- <span class="tag-icon">🔧</span>
|
|
|
- <span class="tag-label">工作阶段</span>
|
|
|
- <span class="tag-count">{{ getTagCount('phase') }}</span>
|
|
|
- </button>
|
|
|
-
|
|
|
- <button
|
|
|
- class="tag-button"
|
|
|
- [class.active]="urgentEventTagFilter === 'review'"
|
|
|
- (click)="filterUrgentEventsByTag('review')"
|
|
|
- title="小图截止"
|
|
|
- >
|
|
|
- <span class="tag-icon">📐</span>
|
|
|
- <span class="tag-label">小图截止</span>
|
|
|
- <span class="tag-count">{{ getTagCount('review') }}</span>
|
|
|
- </button>
|
|
|
-
|
|
|
- <button
|
|
|
- class="tag-button"
|
|
|
- [class.active]="urgentEventTagFilter === 'delivery'"
|
|
|
- (click)="filterUrgentEventsByTag('delivery')"
|
|
|
- title="交付延期"
|
|
|
- >
|
|
|
- <span class="tag-icon">📦</span>
|
|
|
- <span class="tag-label">交付延期</span>
|
|
|
- <span class="tag-count">{{ getTagCount('delivery') }}</span>
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- }
|
|
|
-
|
|
|
- <!-- 加载状态 -->
|
|
|
- @if (loadingUrgentEvents) {
|
|
|
- <div class="loading-state">
|
|
|
- <svg class="spinner" viewBox="0 0 50 50">
|
|
|
- <circle cx="25" cy="25" r="20" fill="none" stroke-width="4"></circle>
|
|
|
- </svg>
|
|
|
- <p>计算紧急事件中...</p>
|
|
|
- </div>
|
|
|
- }
|
|
|
-
|
|
|
- <!-- 空状态 -->
|
|
|
- @if (!loadingUrgentEvents && urgentEvents.length === 0) {
|
|
|
- <div class="empty-state">
|
|
|
- <svg viewBox="0 0 24 24" width="64" height="64" fill="#d1d5db">
|
|
|
- <path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/>
|
|
|
- </svg>
|
|
|
- <p>暂无紧急事件</p>
|
|
|
- <p class="hint">所有项目时间节点正常 ✅</p>
|
|
|
- </div>
|
|
|
- }
|
|
|
-
|
|
|
- @if (!loadingUrgentEvents && urgentEvents.length > 0 && filteredUrgentEventsList.length === 0) {
|
|
|
- <div class="empty-state filtered">
|
|
|
- <svg viewBox="0 0 24 24" width="48" height="48" fill="#d1d5db">
|
|
|
- <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/>
|
|
|
- </svg>
|
|
|
- <p>该筛选条件下暂无事件</p>
|
|
|
- <p class="hint">尝试切换其他标签</p>
|
|
|
- </div>
|
|
|
- }
|
|
|
-
|
|
|
- <!-- 紧急事件列表 -->
|
|
|
- @if (!loadingUrgentEvents && filteredUrgentEventsList.length > 0) {
|
|
|
- <div class="todo-list-compact urgent-list">
|
|
|
- @for (event of filteredUrgentEventsList; track trackUrgentEventById($index, event)) {
|
|
|
- <div class="todo-item-compact urgent-item" [attr.data-urgency]="event.urgencyLevel">
|
|
|
- <!-- 左侧紧急程度色条 -->
|
|
|
- <div class="urgency-indicator" [attr.data-urgency]="event.urgencyLevel"></div>
|
|
|
-
|
|
|
- <!-- 事件内容 -->
|
|
|
- <div class="task-content">
|
|
|
- <!-- 标题行 -->
|
|
|
- <div class="task-header">
|
|
|
- <span class="task-title">{{ event.title }}</span>
|
|
|
- <div class="task-badges">
|
|
|
- <span class="badge badge-urgency" [attr.data-urgency]="event.urgencyLevel">
|
|
|
- @if (event.urgencyLevel === 'critical') { 🔴 紧急 }
|
|
|
- @else if (event.urgencyLevel === 'high') { 🟠 重要 }
|
|
|
- @else { 🟡 注意 }
|
|
|
- </span>
|
|
|
- <span class="badge badge-event-type">
|
|
|
- @if (event.eventType === 'review') { 对图 }
|
|
|
- @else if (event.eventType === 'delivery') { 交付 }
|
|
|
- @else if (event.eventType === 'phase_deadline') { {{ event.phaseName }} }
|
|
|
- @else if (getEventCategory(event) === 'customer') { 客户 }
|
|
|
- </span>
|
|
|
- <span class="badge-status overdue" *ngIf="event.statusType === 'overdue'">逾期</span>
|
|
|
- <span class="badge-status upcoming" *ngIf="event.statusType === 'dueSoon'">临近</span>
|
|
|
- <span class="badge-status stagnant" *ngIf="event.statusType === 'stagnant'">
|
|
|
- 停滞{{ event.stagnationDays || 7 }}天
|
|
|
- </span>
|
|
|
- <span class="badge-status customer" *ngIf="getEventCategory(event) === 'customer'">客户预警</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 描述 -->
|
|
|
- <div class="task-description">
|
|
|
- {{ event.description }}
|
|
|
- </div>
|
|
|
- @if (event.followUpNeeded) {
|
|
|
- <div class="followup-tip">
|
|
|
- 客户反馈待跟进 · 请及时追踪
|
|
|
- </div>
|
|
|
- }
|
|
|
-
|
|
|
- <!-- 项目信息行 -->
|
|
|
- <div class="task-meta">
|
|
|
- <span class="project-info">
|
|
|
- <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
|
|
|
- <path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
|
|
|
- </svg>
|
|
|
- 项目: {{ event.projectName }}
|
|
|
- </span>
|
|
|
- @if (event.designerName) {
|
|
|
- <span class="designer-info">
|
|
|
- <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
|
|
|
- <path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
|
|
|
- </svg>
|
|
|
- 设计师: {{ event.designerName }}
|
|
|
- </span>
|
|
|
- }
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 底部信息行 -->
|
|
|
- <div class="task-footer">
|
|
|
- <span class="deadline-info" [class.overdue]="event.overdueDays && event.overdueDays > 0">
|
|
|
- <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
|
|
|
- <path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z"/>
|
|
|
- </svg>
|
|
|
- 截止: {{ event.deadline | date:'MM-dd HH:mm' }}
|
|
|
- @if (event.overdueDays && event.overdueDays > 0) {
|
|
|
- <span class="overdue-label">(逾期{{ event.overdueDays }}天)</span>
|
|
|
- }
|
|
|
- @else if (event.overdueDays && event.overdueDays < 0) {
|
|
|
- <span class="upcoming-label">(还剩{{ -event.overdueDays }}天)</span>
|
|
|
- }
|
|
|
- @else {
|
|
|
- <span class="today-label">(今天)</span>
|
|
|
- }
|
|
|
- </span>
|
|
|
-
|
|
|
- @if (event.completionRate !== undefined) {
|
|
|
- <span class="completion-info">
|
|
|
- <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
|
|
|
- <path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/>
|
|
|
- </svg>
|
|
|
- 完成率: {{ event.completionRate }}%
|
|
|
- </span>
|
|
|
- }
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 右侧操作按钮 -->
|
|
|
- <div class="task-actions">
|
|
|
- <button
|
|
|
- class="btn-action btn-muted"
|
|
|
- *ngIf="event.allowConfirmOnTime"
|
|
|
- (click)="confirmEventOnTime(event)"
|
|
|
- >
|
|
|
- 可按时交付
|
|
|
- </button>
|
|
|
- <button
|
|
|
- class="btn-action btn-stagnant"
|
|
|
- *ngIf="event.statusType !== 'stagnant'"
|
|
|
- (click)="markEventAsStagnant(event)"
|
|
|
- >
|
|
|
- 标记停滞
|
|
|
- </button>
|
|
|
- <button
|
|
|
- class="btn-action btn-resolve"
|
|
|
- *ngIf="event.allowMarkHandled"
|
|
|
- (click)="resolveUrgentEvent(event)"
|
|
|
- >
|
|
|
- 事件已处理
|
|
|
- </button>
|
|
|
- <button
|
|
|
- class="btn-action btn-todo"
|
|
|
- *ngIf="event.allowCreateTodo"
|
|
|
- (click)="createTodoFromEvent(event)"
|
|
|
- >
|
|
|
- 创建代办
|
|
|
- </button>
|
|
|
- <button
|
|
|
- class="btn-action btn-view"
|
|
|
- (click)="onProjectClick(event.projectId)"
|
|
|
- title="查看项目">
|
|
|
- <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
|
|
|
- <path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/>
|
|
|
- </svg>
|
|
|
- 查看项目
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- }
|
|
|
- </div>
|
|
|
- }
|
|
|
- </div>
|
|
|
- <!-- ========== 右栏结束 ========== -->
|
|
|
- </div>
|
|
|
- <!-- ========== 双栏容器结束 ========== -->
|
|
|
- </section>
|
|
|
+ <!-- 待办任务双栏布局(待办问题 + 紧急事件) -->
|
|
|
+ <app-todo-section
|
|
|
+ [todoTasksFromIssues]="todoTasksFromIssues"
|
|
|
+ [loadingTodoTasks]="loadingTodoTasks"
|
|
|
+ [todoTaskError]="todoTaskError"
|
|
|
+ [urgentEvents]="urgentEvents"
|
|
|
+ [loadingUrgentEvents]="loadingUrgentEvents"
|
|
|
+ (refresh)="refreshTodoTasks()"
|
|
|
+ (navigateToIssue)="navigateToIssue($event)"
|
|
|
+ (markAsRead)="markAsRead($event)"
|
|
|
+ (projectClick)="onProjectClick($event)"
|
|
|
+ (confirmEventOnTime)="confirmEventOnTime($event)"
|
|
|
+ (markEventAsStagnant)="markEventAsStagnant($event)"
|
|
|
+ (resolveUrgentEvent)="resolveUrgentEvent($event)"
|
|
|
+ (createTodoFromEvent)="createTodoFromEvent($event)">
|
|
|
+ </app-todo-section>
|
|
|
|
|
|
- <!-- 超期项目提醒 -->
|
|
|
- @if (showAlert && overdueProjects.length > 0) {
|
|
|
- <div class="overdue-alert">
|
|
|
- <div class="alert-content">
|
|
|
- <h3>⚠️ 超期项目提醒</h3>
|
|
|
- <ul>
|
|
|
- @for (project of overdueProjects.slice(0, 3); track $index) {
|
|
|
- <li>
|
|
|
- {{ project.name }} ({{ project.designerName }} 负责) - 超期{{ project.overdueDays }}天
|
|
|
- </li>
|
|
|
- }
|
|
|
- </ul>
|
|
|
- <div class="alert-actions">
|
|
|
- <button (click)="viewAllOverdueProjects()" class="btn-view-all">查看全部</button>
|
|
|
- <button (click)="closeAlert()" class="btn-close">关闭</button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- }
|
|
|
- @if (urgentPinnedProjects && urgentPinnedProjects.length > 0) {
|
|
|
- <div class="urgent-pinned">
|
|
|
- <div class="pinned-title">紧急任务固定区(超期 + 高紧急)</div>
|
|
|
- <div class="pinned-list">
|
|
|
- @for (p of urgentPinnedProjects.slice(0, 3); track $index) {
|
|
|
- <div class="pinned-item" (click)="filterByStatus('overdue')">
|
|
|
- <span class="dot dot-high"></span>
|
|
|
- <span class="name">{{ p.name }}</span>
|
|
|
- <span class="meta">{{ p.designerName || '未分配' }} · 超期{{ p.overdueDays }}天</span>
|
|
|
- </div>
|
|
|
- }
|
|
|
- @if (urgentPinnedProjects.length > 3) {
|
|
|
- <button class="btn-view-all" (click)="viewAllOverdueProjects()">更多…</button>
|
|
|
- }
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- }
|
|
|
+ <!-- 超期/紧急项目提醒组件 -->
|
|
|
+ <app-dashboard-alerts
|
|
|
+ [showAlert]="showAlert"
|
|
|
+ [overdueProjects]="overdueProjects"
|
|
|
+ [urgentPinnedProjects]="urgentPinnedProjects"
|
|
|
+ (viewAllOverdue)="viewAllOverdueProjects()"
|
|
|
+ (closeAlert)="closeAlert()"
|
|
|
+ (filterStatus)="filterByStatus($event)">
|
|
|
+ </app-dashboard-alerts>
|
|
|
</main>
|
|
|
|
|
|
<!-- 员工详情面板组件 -->
|
|
|
<app-employee-detail-panel
|
|
|
[visible]="showEmployeeDetailPanel"
|
|
|
- [employeeDetail]="selectedEmployeeDetail"
|
|
|
+ [employeeName]="selectedEmployeeName"
|
|
|
+ [projects]="selectedEmployeeProjects"
|
|
|
(close)="closeEmployeeDetailPanel()"
|
|
|
- (calendarMonthChange)="changeEmployeeCalendarMonth($event)"
|
|
|
- (calendarDayClick)="onCalendarDayClick($event)"
|
|
|
- (projectClick)="navigateToProjectFromPanel($event)"
|
|
|
- (refreshSurvey)="refreshEmployeeSurvey()">
|
|
|
+ (projectClick)="navigateToProjectFromPanel($event)">
|
|
|
</app-employee-detail-panel>
|
|
|
|
|
|
-<!-- 以下代码已由 EmployeeDetailPanelComponent 组件替代,日历项目列表弹窗也已集成到组件内部 -->
|
|
|
-<!--
|
|
|
-<!-- 员工详情面板(旧代码已废弃) -->
|
|
|
-@if (showEmployeeDetailPanel && selectedEmployeeDetail) {
|
|
|
- <div class="employee-detail-overlay" (click)="closeEmployeeDetailPanel()">
|
|
|
- <div class="employee-detail-panel" (click)="$event.stopPropagation()">
|
|
|
- <!-- 面板头部 -->
|
|
|
- <div class="panel-header">
|
|
|
- <h3 class="panel-title">
|
|
|
- <svg class="icon-user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
- <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
|
|
- <circle cx="12" cy="7" r="4"></circle>
|
|
|
- </svg>
|
|
|
- {{ selectedEmployeeDetail.name }} 详情
|
|
|
- </h3>
|
|
|
- <button class="btn-close" (click)="closeEmployeeDetailPanel()">
|
|
|
- <svg 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="panel-content">
|
|
|
- <!-- 负载概况栏 -->
|
|
|
- <div class="section workload-section">
|
|
|
- <div class="section-header">
|
|
|
- <svg class="section-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
- <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
|
|
- <line x1="9" y1="9" x2="15" y2="9"></line>
|
|
|
- <line x1="9" y1="15" x2="15" y2="15"></line>
|
|
|
- </svg>
|
|
|
- <h4>负载概况</h4>
|
|
|
- </div>
|
|
|
- <div class="workload-info">
|
|
|
- <div class="workload-stat">
|
|
|
- <span class="stat-label">当前负责项目数:</span>
|
|
|
- <span class="stat-value" [class]="selectedEmployeeDetail.currentProjects >= 3 ? 'high-workload' : 'normal-workload'">
|
|
|
- {{ selectedEmployeeDetail.currentProjects }} 个
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- @if (selectedEmployeeDetail.projectData.length > 0) {
|
|
|
- <div class="project-list">
|
|
|
- <span class="project-label">核心项目:</span>
|
|
|
- <div class="project-tags">
|
|
|
- @for (project of selectedEmployeeDetail.projectData; track project.id) {
|
|
|
- <span class="project-tag clickable"
|
|
|
- (click)="navigateToProjectFromPanel(project.id)"
|
|
|
- title="点击查看项目详情">
|
|
|
- {{ project.name }}
|
|
|
- <svg class="icon-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
- <path d="M7 17L17 7M17 7H7M17 7V17"/>
|
|
|
- </svg>
|
|
|
- </span>
|
|
|
- }
|
|
|
- @if (selectedEmployeeDetail.currentProjects > selectedEmployeeDetail.projectData.length) {
|
|
|
- <span class="project-tag more">+{{ selectedEmployeeDetail.currentProjects - selectedEmployeeDetail.projectData.length }}</span>
|
|
|
- }
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- }
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 负载详细日历 -->
|
|
|
- <div class="section calendar-section">
|
|
|
- <div class="section-header">
|
|
|
- <svg class="section-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
- <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
|
|
|
- <line x1="16" y1="2" x2="16" y2="6"></line>
|
|
|
- <line x1="8" y1="2" x2="8" y2="6"></line>
|
|
|
- <line x1="3" y1="10" x2="21" y2="10"></line>
|
|
|
- </svg>
|
|
|
- <h4>负载详细日历</h4>
|
|
|
- </div>
|
|
|
-
|
|
|
- @if (selectedEmployeeDetail.calendarData) {
|
|
|
- <div class="employee-calendar">
|
|
|
- <!-- 月份标题 -->
|
|
|
- <div class="calendar-month-header">
|
|
|
- <button class="btn-prev-month"
|
|
|
- (click)="changeEmployeeCalendarMonth(-1)"
|
|
|
- title="上月">
|
|
|
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
- <polyline points="15 18 9 12 15 6"></polyline>
|
|
|
- </svg>
|
|
|
- </button>
|
|
|
- <span class="month-title">
|
|
|
- {{ selectedEmployeeDetail.calendarData.currentMonth | date:'yyyy年M月' }}
|
|
|
- </span>
|
|
|
- <button class="btn-next-month"
|
|
|
- (click)="changeEmployeeCalendarMonth(1)"
|
|
|
- title="下月">
|
|
|
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
- <polyline points="9 18 15 12 9 6"></polyline>
|
|
|
- </svg>
|
|
|
- </button>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 星期标题 -->
|
|
|
- <div class="calendar-weekdays">
|
|
|
- <div class="weekday">日</div>
|
|
|
- <div class="weekday">一</div>
|
|
|
- <div class="weekday">二</div>
|
|
|
- <div class="weekday">三</div>
|
|
|
- <div class="weekday">四</div>
|
|
|
- <div class="weekday">五</div>
|
|
|
- <div class="weekday">六</div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 日历网格 -->
|
|
|
- <div class="calendar-grid">
|
|
|
- @for (day of selectedEmployeeDetail.calendarData.days; track day.date.getTime()) {
|
|
|
- <div class="calendar-day"
|
|
|
- [class.today]="day.isToday"
|
|
|
- [class.other-month]="!day.isCurrentMonth"
|
|
|
- [class.has-projects]="day.projectCount > 0"
|
|
|
- [class.clickable]="day.projectCount > 0 && day.isCurrentMonth"
|
|
|
- (click)="onCalendarDayClick(day)">
|
|
|
- <div class="day-number">{{ day.date.getDate() }}</div>
|
|
|
- @if (day.projectCount > 0) {
|
|
|
- <div class="day-badge" [class.high-load]="day.projectCount >= 2">
|
|
|
- {{ day.projectCount }}个项目
|
|
|
- </div>
|
|
|
- }
|
|
|
- </div>
|
|
|
- }
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 图例 -->
|
|
|
- <div class="calendar-legend">
|
|
|
- <div class="legend-item">
|
|
|
- <span class="legend-dot today-dot"></span>
|
|
|
- <span class="legend-text">今天</span>
|
|
|
- </div>
|
|
|
- <div class="legend-item">
|
|
|
- <span class="legend-dot project-dot"></span>
|
|
|
- <span class="legend-text">有项目</span>
|
|
|
- </div>
|
|
|
- <div class="legend-item">
|
|
|
- <span class="legend-dot high-dot"></span>
|
|
|
- <span class="legend-text">高负载</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- }
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 请假明细栏 -->
|
|
|
- <div class="section leave-section">
|
|
|
- <div class="section-header">
|
|
|
- <svg class="section-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
- <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
|
|
|
- <line x1="16" y1="2" x2="16" y2="6"></line>
|
|
|
- <line x1="8" y1="2" x2="8" y2="6"></line>
|
|
|
- <line x1="3" y1="10" x2="21" y2="10"></line>
|
|
|
- </svg>
|
|
|
- <h4>请假明细(未来7天)</h4>
|
|
|
- </div>
|
|
|
- <div class="leave-table">
|
|
|
- @if (selectedEmployeeDetail.leaveRecords.length > 0) {
|
|
|
- <table>
|
|
|
- <thead>
|
|
|
- <tr>
|
|
|
- <th>日期</th>
|
|
|
- <th>状态</th>
|
|
|
- <th>备注</th>
|
|
|
- </tr>
|
|
|
- </thead>
|
|
|
- <tbody>
|
|
|
- @for (record of selectedEmployeeDetail.leaveRecords; track record.id) {
|
|
|
- <tr [class]="record.isLeave ? 'leave-day' : 'work-day'">
|
|
|
- <td>{{ record.date | date:'M月d日' }}</td>
|
|
|
- <td>
|
|
|
- <span class="status-badge" [class]="record.isLeave ? 'leave' : 'work'">
|
|
|
- {{ record.isLeave ? '请假' : '正常' }}
|
|
|
- </span>
|
|
|
- </td>
|
|
|
- <td>{{ record.isLeave ? getLeaveTypeText(record.leaveType) : '-' }}</td>
|
|
|
- </tr>
|
|
|
- }
|
|
|
- </tbody>
|
|
|
- </table>
|
|
|
- } @else {
|
|
|
- <div class="no-leave">
|
|
|
- <svg class="no-data-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
- <circle cx="12" cy="12" r="10"></circle>
|
|
|
- <path d="M8 14s1.5 2 4 2 4-2 4-2"></path>
|
|
|
- <line x1="9" y1="9" x2="9.01" y2="9"></line>
|
|
|
- <line x1="15" y1="9" x2="15.01" y2="9"></line>
|
|
|
- </svg>
|
|
|
- <p>未来7天无请假安排</p>
|
|
|
- </div>
|
|
|
- }
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 红色标记说明 -->
|
|
|
- <div class="section explanation-section">
|
|
|
- <div class="section-header">
|
|
|
- <svg class="section-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
- <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>
|
|
|
- <h4>红色标记说明</h4>
|
|
|
- </div>
|
|
|
- <div class="explanation-content">
|
|
|
- <p class="explanation-text">{{ selectedEmployeeDetail.redMarkExplanation }}</p>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 新增:能力问卷 -->
|
|
|
- <div class="section survey-section">
|
|
|
- <div class="section-header">
|
|
|
- <svg class="section-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
- <path d="M19,3H14.82C14.4,1.84 13.3,1 12,1C10.7,1 9.6,1.84 9.18,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3M12,3A1,1 0 0,1 13,4A1,1 0 0,1 12,5A1,1 0 0,1 11,4A1,1 0 0,1 12,3"/>
|
|
|
- </svg>
|
|
|
- <h4>能力问卷</h4>
|
|
|
- <button
|
|
|
- class="btn-refresh-survey"
|
|
|
- (click)="refreshEmployeeSurvey()"
|
|
|
- [disabled]="refreshingSurvey"
|
|
|
- title="刷新问卷状态">
|
|
|
- <svg viewBox="0 0 24 24" width="16" height="16" [class.rotating]="refreshingSurvey">
|
|
|
- <path fill="currentColor" d="M17.65 6.35A7.958 7.958 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>
|
|
|
- </svg>
|
|
|
- </button>
|
|
|
- </div>
|
|
|
-
|
|
|
- @if (selectedEmployeeDetail.surveyCompleted && selectedEmployeeDetail.surveyData) {
|
|
|
- <div class="survey-content">
|
|
|
- <div class="survey-status completed">
|
|
|
- <svg viewBox="0 0 24 24" width="20" height="20" fill="#34c759">
|
|
|
- <path d="M9,20.42L2.79,14.21L5.62,11.38L9,14.77L18.88,4.88L21.71,7.71L9,20.42Z"/>
|
|
|
- </svg>
|
|
|
- <span>已完成问卷</span>
|
|
|
- <span class="survey-time">
|
|
|
- {{ selectedEmployeeDetail.surveyData.createdAt | date:'yyyy-MM-dd HH:mm' }}
|
|
|
- </span>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 能力画像摘要 -->
|
|
|
- @if (!showFullSurvey) {
|
|
|
- <div class="capability-summary">
|
|
|
- <h5>您的能力画像</h5>
|
|
|
- @if (getCapabilitySummary(selectedEmployeeDetail.surveyData.answers); as summary) {
|
|
|
- <div class="summary-grid">
|
|
|
- <div class="summary-item">
|
|
|
- <span class="label">擅长风格:</span>
|
|
|
- <span class="value">{{ summary.styles }}</span>
|
|
|
- </div>
|
|
|
- <div class="summary-item">
|
|
|
- <span class="label">擅长空间:</span>
|
|
|
- <span class="value">{{ summary.spaces }}</span>
|
|
|
- </div>
|
|
|
- <div class="summary-item">
|
|
|
- <span class="label">技术优势:</span>
|
|
|
- <span class="value">{{ summary.advantages }}</span>
|
|
|
- </div>
|
|
|
- <div class="summary-item">
|
|
|
- <span class="label">项目难度:</span>
|
|
|
- <span class="value">{{ summary.difficulty }}</span>
|
|
|
- </div>
|
|
|
- <div class="summary-item">
|
|
|
- <span class="label">周承接量:</span>
|
|
|
- <span class="value">{{ summary.capacity }}</span>
|
|
|
- </div>
|
|
|
- <div class="summary-item">
|
|
|
- <span class="label">紧急订单:</span>
|
|
|
- <span class="value">
|
|
|
- {{ summary.urgent }}
|
|
|
- @if (summary.urgentLimit) {
|
|
|
- <span class="limit-hint">(每月不超过{{summary.urgentLimit}}次)</span>
|
|
|
- }
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- <div class="summary-item">
|
|
|
- <span class="label">进度同步:</span>
|
|
|
- <span class="value">{{ summary.feedback }}</span>
|
|
|
- </div>
|
|
|
- <div class="summary-item">
|
|
|
- <span class="label">沟通方式:</span>
|
|
|
- <span class="value">{{ summary.communication }}</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- }
|
|
|
-
|
|
|
- <button class="btn-view-full" (click)="toggleSurveyDisplay()">
|
|
|
- <svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
|
|
|
- <path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
|
|
- </svg>
|
|
|
- 查看完整问卷(共 {{ selectedEmployeeDetail.surveyData.answers.length }} 道题)
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- }
|
|
|
-
|
|
|
- <!-- 完整问卷答案 -->
|
|
|
- @if (showFullSurvey) {
|
|
|
- <div class="survey-answers">
|
|
|
- <h5>完整问卷答案(共 {{ selectedEmployeeDetail.surveyData.answers.length }} 道题):</h5>
|
|
|
- @for (answer of selectedEmployeeDetail.surveyData.answers; track $index) {
|
|
|
- <div class="answer-item">
|
|
|
- <div class="question-text">
|
|
|
- <strong>Q{{$index + 1}}:</strong> {{ answer.question }}
|
|
|
- </div>
|
|
|
- <div class="answer-text">
|
|
|
- @if (!answer.answer) {
|
|
|
- <span class="answer-tag empty">未填写(选填)</span>
|
|
|
- } @else if (answer.type === 'single' || answer.type === 'text' || answer.type === 'textarea' || answer.type === 'number') {
|
|
|
- <span class="answer-tag single">{{ answer.answer }}</span>
|
|
|
- } @else if (answer.type === 'multiple') {
|
|
|
- @if (Array.isArray(answer.answer)) {
|
|
|
- @for (opt of answer.answer; track opt) {
|
|
|
- <span class="answer-tag multiple">{{ opt }}</span>
|
|
|
- }
|
|
|
- } @else {
|
|
|
- <span class="answer-tag single">{{ answer.answer }}</span>
|
|
|
- }
|
|
|
- } @else if (answer.type === 'scale') {
|
|
|
- <div class="answer-scale">
|
|
|
- <div class="scale-bar">
|
|
|
- <div class="scale-fill" [style.width.%]="(answer.answer / 10) * 100">
|
|
|
- <span>{{ answer.answer }} / 10</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- } @else {
|
|
|
- <span class="answer-tag single">{{ answer.answer }}</span>
|
|
|
- }
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- }
|
|
|
-
|
|
|
- <button class="btn-collapse" (click)="toggleSurveyDisplay()">
|
|
|
- <svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
|
|
|
- <path d="M19 13H5v-2h14v2z"/>
|
|
|
- </svg>
|
|
|
- 收起详情
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- }
|
|
|
- </div>
|
|
|
- } @else {
|
|
|
- <div class="survey-empty">
|
|
|
- <svg class="no-data-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
- <circle cx="12" cy="12" r="10"></circle>
|
|
|
- <path d="M8 12h8M12 8v8"/>
|
|
|
- </svg>
|
|
|
- <p>该员工尚未完成能力问卷</p>
|
|
|
- </div>
|
|
|
- }
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-}
|
|
|
-
|
|
|
-<!-- 日历项目列表弹窗 -->
|
|
|
-@if (showCalendarProjectList) {
|
|
|
- <div class="calendar-project-modal-overlay" (click)="closeCalendarProjectList()">
|
|
|
- <div class="calendar-project-modal" (click)="$event.stopPropagation()">
|
|
|
- <div class="modal-header">
|
|
|
- <h3>
|
|
|
- <svg class="header-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
- <path d="M9 11l3 3L22 4"></path>
|
|
|
- <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
|
|
|
- </svg>
|
|
|
- {{ selectedDate | date:'M月d日' }} 的项目
|
|
|
- </h3>
|
|
|
- <button class="btn-close" (click)="closeCalendarProjectList()">
|
|
|
- <svg 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="modal-body">
|
|
|
- <div class="project-count-info">
|
|
|
- 共 <strong>{{ selectedDayProjects.length }}</strong> 个项目
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="project-list">
|
|
|
- @for (project of selectedDayProjects; track project.id) {
|
|
|
- <div class="project-item" (click)="navigateToProjectFromPanel(project.id); closeCalendarProjectList()">
|
|
|
- <div class="project-info">
|
|
|
- <svg class="project-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
- <path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path>
|
|
|
- <path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path>
|
|
|
- </svg>
|
|
|
- <div class="project-details">
|
|
|
- <h4 class="project-name">{{ project.name }}</h4>
|
|
|
- @if (project.deadline) {
|
|
|
- <p class="project-deadline">
|
|
|
- 截止日期: {{ project.deadline | date:'yyyy-MM-dd' }}
|
|
|
- </p>
|
|
|
- }
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <svg class="arrow-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
- <path d="M5 12h14M12 5l7 7-7 7"/>
|
|
|
- </svg>
|
|
|
- </div>
|
|
|
- }
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-}
|
|
|
--->
|
|
|
-
|
|
|
<!-- 智能推荐弹窗 -->
|
|
|
-@if (showSmartMatch) {
|
|
|
- <div class="smart-match-modal">
|
|
|
- <div class="modal-backdrop" (click)="closeSmartMatch()"></div>
|
|
|
- <div class="modal-content">
|
|
|
- <div class="modal-header">
|
|
|
- <h3>🤖 智能推荐设计师</h3>
|
|
|
- <button class="btn-close" (click)="closeSmartMatch()">×</button>
|
|
|
- </div>
|
|
|
-
|
|
|
- @if (selectedProject) {
|
|
|
- <div class="project-info">
|
|
|
- <h4>{{ selectedProject.name }}</h4>
|
|
|
- <div class="tags">
|
|
|
- <span class="tag">{{ selectedProject.type === 'hard' ? '硬装' : '软装' }}</span>
|
|
|
- <span class="tag">{{ selectedProject.memberType === 'vip' ? 'VIP' : '普通' }}</span>
|
|
|
- <span class="tag urgency u-{{ selectedProject.urgency }}">
|
|
|
- {{ getUrgencyLabel(selectedProject.urgency) }}
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- }
|
|
|
-
|
|
|
- <div class="recommendations-list">
|
|
|
- @for (rec of recommendations; track rec.designer.id; let i = $index) {
|
|
|
- <div class="rec-card">
|
|
|
- <div class="rank" [class.gold]="i===0" [class.silver]="i===1" [class.bronze]="i===2">
|
|
|
- {{ i + 1 }}
|
|
|
- </div>
|
|
|
- <div class="designer-info">
|
|
|
- <h4>{{ rec.designer.name }}</h4>
|
|
|
- <div class="match-score-bar">
|
|
|
- <div class="score-fill" [style.width.%]="rec.matchScore">
|
|
|
- <span>{{ rec.matchScore }}分</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div class="details">
|
|
|
- <p><strong>擅长:</strong>{{ rec.designer.tags.expertise.styles.join('、') || '暂无标签' }}</p>
|
|
|
- <p><strong>负载:</strong>{{ rec.loadRate.toFixed(0) }}% ({{ rec.currentProjects }}个项目)</p>
|
|
|
- <p><strong>评分:</strong>⭐ {{ rec.designer.tags.history.avgRating || '暂无' }}</p>
|
|
|
- <p class="reason"><strong>推荐理由:</strong>{{ rec.reason }}</p>
|
|
|
- </div>
|
|
|
- <button class="btn-assign" (click)="assignToDesigner(rec.designer.id)">
|
|
|
- 分配给TA
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- }
|
|
|
-
|
|
|
- @if (recommendations.length === 0) {
|
|
|
- <div class="empty">
|
|
|
- <p>未找到合适的设计师</p>
|
|
|
- <p>您可以手动分配或调整项目参数</p>
|
|
|
- </div>
|
|
|
- }
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-}
|
|
|
+<app-smart-match-modal
|
|
|
+ [visible]="showSmartMatch"
|
|
|
+ [selectedProject]="selectedProject"
|
|
|
+ [recommendations]="recommendations"
|
|
|
+ (close)="closeSmartMatch()"
|
|
|
+ (assign)="assignToDesigner($event)">
|
|
|
+</app-smart-match-modal>
|