project-timeline.html 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. <div class="project-timeline-container">
  2. <!-- 顶部筛选栏 -->
  3. <div class="timeline-header">
  4. <div class="filter-section">
  5. <!-- 设计师选择 -->
  6. <div class="filter-group">
  7. <label>设计师:</label>
  8. <select [(ngModel)]="selectedDesigner" (change)="applyFilters()" class="filter-select">
  9. <option value="all">全部设计师</option>
  10. @for (designer of designers; track designer.id) {
  11. <option [value]="designer.id">
  12. {{ designer.name }} ({{ designer.projectCount }})
  13. </option>
  14. }
  15. </select>
  16. </div>
  17. <!-- 快捷筛选按钮 -->
  18. <div class="filter-group quick-filters">
  19. <button
  20. class="filter-btn"
  21. [class.active]="selectedStatus === 'overdue'"
  22. (click)="toggleFilter('status', 'overdue')">
  23. 🔴 逾期
  24. </button>
  25. <button
  26. class="filter-btn"
  27. [class.active]="selectedStatus === 'urgent'"
  28. (click)="toggleFilter('status', 'urgent')">
  29. 🟠 紧急
  30. </button>
  31. <button
  32. class="filter-btn"
  33. [class.active]="selectedStatus === 'stalled'"
  34. (click)="toggleFilter('status', 'stalled')">
  35. ⏸️ 停滞
  36. </button>
  37. </div>
  38. <!-- 时间尺度切换 -->
  39. <div class="filter-group time-scale-controls">
  40. <label>时间范围:</label>
  41. <button
  42. class="scale-btn"
  43. [class.active]="timelineScale === 'week'"
  44. (click)="toggleTimelineScale('week')">
  45. 📆 7天
  46. </button>
  47. <button
  48. class="scale-btn"
  49. [class.active]="timelineScale === 'month'"
  50. (click)="toggleTimelineScale('month')">
  51. 📅 30天
  52. </button>
  53. </div>
  54. <!-- 🆕 手动刷新按钮 -->
  55. <div class="filter-group refresh-controls">
  56. <button
  57. class="refresh-btn"
  58. (click)="refresh()"
  59. title="刷新数据和时间线(自动10分钟刷新一次)">
  60. 🔄 刷新
  61. </button>
  62. </div>
  63. <!-- 排序方式 -->
  64. <div class="filter-group sort-controls">
  65. <button
  66. class="sort-btn"
  67. [class.active]="sortBy === 'priority'"
  68. (click)="toggleSortBy('priority')">
  69. 按优先级
  70. </button>
  71. <button
  72. class="sort-btn"
  73. [class.active]="sortBy === 'time'"
  74. (click)="toggleSortBy('time')">
  75. 按时间
  76. </button>
  77. </div>
  78. </div>
  79. <!-- 设计师统计面板(选中时显示) -->
  80. @if (selectedDesigner !== 'all') {
  81. <div class="designer-stats-panel">
  82. @if (getSelectedDesigner(); as designer) {
  83. <div class="stats-header">
  84. <h3>{{ designer.name }}</h3>
  85. <span class="workload-badge" [class]="'level-' + designer.workloadLevel">
  86. {{ getWorkloadIcon(designer.workloadLevel) }}
  87. @if (designer.workloadLevel === 'high') { 超负荷 }
  88. @else if (designer.workloadLevel === 'medium') { 适度忙碌 }
  89. @else { 空闲 }
  90. </span>
  91. </div>
  92. <div class="stats-body">
  93. <div class="stat-item">
  94. <span class="stat-label">总项目数</span>
  95. <span class="stat-value">{{ designer.projectCount }}</span>
  96. </div>
  97. <div class="stat-item">
  98. <span class="stat-label">紧急项目</span>
  99. <span class="stat-value urgent">{{ designer.urgentCount }}</span>
  100. </div>
  101. <div class="stat-item">
  102. <span class="stat-label">逾期项目</span>
  103. <span class="stat-value overdue">{{ designer.overdueCount }}</span>
  104. </div>
  105. </div>
  106. }
  107. </div>
  108. }
  109. </div>
  110. <!-- 时间轴主体 -->
  111. <div class="timeline-body timeline-view">
  112. <!-- 时间轴视图 -->
  113. <div class="timeline-view-container">
  114. <!-- 图例说明 -->
  115. <div class="timeline-legend">
  116. <div class="legend-item">
  117. <span class="legend-icon start-icon">▶️</span>
  118. <span class="legend-label">项目开始</span>
  119. </div>
  120. <div class="legend-item">
  121. <span class="legend-icon review-icon">📋</span>
  122. <span class="legend-label">小图对图(灰色=已完成)</span>
  123. </div>
  124. <div class="legend-item">
  125. <span class="legend-icon delivery-icon">📦</span>
  126. <span class="legend-label">交付日期</span>
  127. </div>
  128. <div class="legend-separator"></div>
  129. <div class="legend-item legend-phase">
  130. <span class="legend-icon phase-icon modeling-icon">建</span>
  131. <span class="legend-label">建模截止</span>
  132. </div>
  133. <div class="legend-item legend-phase">
  134. <span class="legend-icon phase-icon softDecor-icon">软</span>
  135. <span class="legend-label">软装截止</span>
  136. </div>
  137. <div class="legend-item legend-phase">
  138. <span class="legend-icon phase-icon rendering-icon">渲</span>
  139. <span class="legend-label">渲染截止</span>
  140. </div>
  141. <div class="legend-item legend-phase">
  142. <span class="legend-icon phase-icon postProcessing-icon">后</span>
  143. <span class="legend-label">后期截止</span>
  144. </div>
  145. <div class="legend-separator"></div>
  146. <div class="legend-item">
  147. <div class="legend-bar-demo legend-bar-green"></div>
  148. <span class="legend-label">🟢 正常进行(2天+)</span>
  149. </div>
  150. <div class="legend-item">
  151. <div class="legend-bar-demo legend-bar-yellow"></div>
  152. <span class="legend-label">🟡 前一天(24小时内)</span>
  153. </div>
  154. <div class="legend-item">
  155. <div class="legend-bar-demo legend-bar-orange"></div>
  156. <span class="legend-label">🟠 事件当天(6小时+)</span>
  157. </div>
  158. <div class="legend-item">
  159. <div class="legend-bar-demo legend-bar-red"></div>
  160. <span class="legend-label">🔴 紧急(6小时内)</span>
  161. </div>
  162. <div class="legend-item legend-note">
  163. <span class="legend-label">💡 仅显示今日线之后的关键事件和阶段截止时间</span>
  164. </div>
  165. </div>
  166. <!-- 时间刻度尺 -->
  167. <div class="timeline-ruler">
  168. <div class="ruler-header">
  169. <span class="project-name-header">项目名称</span>
  170. </div>
  171. <div class="ruler-ticks">
  172. @for (date of timeRange; track date; let i = $index) {
  173. <div class="ruler-tick" [class.first]="i === 0">
  174. <div class="tick-date">{{ date.getMonth() + 1 }}/{{ date.getDate() }}</div>
  175. @if (timelineScale === 'week') {
  176. <div class="tick-weekday">
  177. @switch (date.getDay()) {
  178. @case (0) { 周日 }
  179. @case (1) { 周一 }
  180. @case (2) { 周二 }
  181. @case (3) { 周三 }
  182. @case (4) { 周四 }
  183. @case (5) { 周五 }
  184. @case (6) { 周六 }
  185. }
  186. </div>
  187. }
  188. </div>
  189. }
  190. </div>
  191. </div>
  192. <!-- 今日标记线(实时移动,精确到分钟) -->
  193. <div class="today-line"
  194. [style.left]="getTodayPosition()">
  195. <div class="today-label">
  196. {{ getTodayLabel() }}
  197. </div>
  198. <div class="today-dot"></div>
  199. <div class="today-bar"></div>
  200. </div>
  201. <!-- 项目时间轴 -->
  202. <div class="timeline-projects">
  203. @if (filteredProjects.length === 0) {
  204. <div class="empty-state">
  205. <p>暂无项目数据</p>
  206. </div>
  207. } @else {
  208. @for (project of filteredProjects; track project.projectId) {
  209. <div class="timeline-row" (click)="onProjectClick(project.projectId)">
  210. <!-- 项目名称标签 -->
  211. <div class="project-label">
  212. <span class="project-name-label" [title]="project.projectName">
  213. {{ project.projectName }}
  214. </span>
  215. <span class="designer-label">{{ project.designerName }}</span>
  216. @if (project.priority === 'critical' || project.priority === 'high') {
  217. <span class="priority-badge" [class]="'badge-' + project.priority">
  218. @if (project.priority === 'critical') { ‼️ }
  219. @else { 🔥 }
  220. </span>
  221. }
  222. <!-- 🆕 空间与交付物统计徽章 -->
  223. @if (getSpaceDeliverableSummary(project.projectId); as summary) {
  224. <span class="space-deliverable-badge"
  225. [title]="formatSpaceDeliverableTooltip(project.projectId)"
  226. [style.background-color]="getProjectDeliveryStatusColor(project.projectId)">
  227. 📦 {{ summary.spacesWithDeliverables }}/{{ summary.totalSpaces }}
  228. </span>
  229. }
  230. </div>
  231. <!-- 时间轴区域 -->
  232. <div class="timeline-track">
  233. <!-- 项目条形图 -->
  234. <div class="project-bar"
  235. [style.left]="getProjectPosition(project).left"
  236. [style.width]="getProjectPosition(project).width"
  237. [style.background]="getProjectPosition(project).background"
  238. [class.status-overdue]="project.status === 'overdue'"
  239. [title]="project.projectName + ' | ' + project.stageName + ' ' + project.stageProgress + '%'">
  240. <!-- 进度填充 -->
  241. <div class="progress-fill" [style.width]="project.stageProgress + '%'"></div>
  242. </div>
  243. <!-- 🆕 使用统一的事件标记方法 -->
  244. @for (event of getProjectEvents(project); track event.date) {
  245. <div class="event-marker"
  246. [class]="event.type"
  247. [class.phase-deadline]="event.type === 'phase_deadline'"
  248. [style.left]="getEventPosition(event.date)"
  249. [style.background]="event.color"
  250. [class.blink]="project.status === 'overdue' && event.type === 'delivery'"
  251. [title]="event.label + ':' + formatTime(event.date) + (event.phase ? ' (' + getPhaseLabel(event.phase) + ')' : '')">
  252. {{ event.icon }}
  253. </div>
  254. }
  255. </div>
  256. </div>
  257. }
  258. }
  259. </div>
  260. </div>
  261. </div>
  262. </div>