dashboard.html 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. <header class="dashboard-header">
  2. <h1>设计组长工作台</h1>
  3. <!-- 核心数据指标卡片 -->
  4. <div class="dashboard-metrics">
  5. <div class="metric-card" (click)="filterByStatus('overdue')">
  6. <div class="metric-icon warning">⚠️</div>
  7. <div class="metric-content">
  8. <div class="metric-count">{{ overdueProjects.length }}</div>
  9. <div class="metric-label">已延期项目</div>
  10. </div>
  11. </div>
  12. <div class="metric-card" (click)="filterByStatus('dueSoon')">
  13. <div class="metric-icon info">⏳</div>
  14. <div class="metric-content">
  15. <div class="metric-count">{{ dueSoonProjects.length }}</div>
  16. <div class="metric-label">临期项目(3天内)</div>
  17. </div>
  18. </div>
  19. <div class="metric-card" (click)="filterByStatus('pendingApproval')">
  20. <div class="metric-icon info">📋</div>
  21. <div class="metric-content">
  22. <div class="metric-count">{{ pendingApprovalProjects.length }}</div>
  23. <div class="metric-label">待组长确认项目</div>
  24. </div>
  25. </div>
  26. <div class="metric-card" (click)="filterByStatus('pendingAssignment')">
  27. <div class="metric-icon primary">🎯</div>
  28. <div class="metric-content">
  29. <div class="metric-count">{{ pendingAssignmentProjects.length }}</div>
  30. <div class="metric-label">待分配方案项目</div>
  31. </div>
  32. </div>
  33. </div>
  34. </header>
  35. <main class="dashboard-main">
  36. <!-- 项目监控大盘 -->
  37. <section class="monitoring-section">
  38. <div class="section-header">
  39. <h2>项目监控大盘</h2>
  40. <div class="section-actions">
  41. @if (selectedStatus !== 'all') {
  42. <button class="btn-link" (click)="resetStatusFilter()">返回全部项目</button>
  43. }
  44. <button class="btn-toggle-view" (click)="toggleView()">{{ showGanttView ? '返回看板' : '切换视图' }}</button>
  45. </div>
  46. </div>
  47. <!-- 新增:工作量概览(与筛选联动,按设计师/会员类型分组,堆叠显示紧急程度) -->
  48. <div class="workload-summary">
  49. <div class="summary-header">
  50. <h3>工作量概览</h3>
  51. <div class="summary-actions">
  52. <div class="dimension-switch">
  53. <button [class.active]="workloadDimension === 'designer'" (click)="setWorkloadDimension('designer')">按设计师</button>
  54. <button [class.active]="workloadDimension === 'member'" (click)="setWorkloadDimension('member')">按会员类型</button>
  55. </div>
  56. </div>
  57. </div>
  58. <div #workloadChartRef class="workload-chart"></div>
  59. </div>
  60. @if (showGanttView) {
  61. <div class="gantt-card">
  62. <div class="gantt-header">
  63. <div class="title">负载日历(甘特)</div>
  64. <div class="hint">
  65. @if (ganttMode === 'project') { 颜色标识紧急程度:红=高,橙=中,绿=低 } @else { 设计师排班:按项目数量由排满到空闲排列 }
  66. </div>
  67. <div class="scale-switch">
  68. <button [class.active]="ganttScale === 'day'" (click)="setGanttScale('day')">日</button>
  69. <button [class.active]="ganttScale === 'week'" (click)="setGanttScale('week')">周</button>
  70. <button [class.active]="ganttScale === 'month'" (click)="setGanttScale('month')">月</button>
  71. </div>
  72. <div class="search-box">
  73. <input type="search" placeholder="搜索项目/设计师/风格关键词" [(ngModel)]="searchTerm" (input)="onSearchChange()" (focus)="onSearchFocus()" (blur)="onSearchBlur()" />
  74. @if (showSuggestions) {
  75. <div class="suggestion-panel">
  76. @if (searchSuggestions.length > 0) {
  77. <ul>
  78. @for (suggest of searchSuggestions; track suggest.id) {
  79. <li (mousedown)="selectSuggestion(suggest)">
  80. <div class="line-1">
  81. <span class="name">{{ suggest.name }}</span>
  82. <span class="badge" [class.vip]="suggest.memberType==='vip'">{{ suggest.memberType==='vip' ? 'VIP' : '普通' }}</span>
  83. <span class="urgency" [class]="'u-' + suggest.urgency">{{ getUrgencyLabel(suggest.urgency) }}</span>
  84. </div>
  85. <div class="line-2">
  86. <span class="designer">{{ suggest.designerName || '未分配' }}</span>
  87. <span class="deadline">{{ suggest.deadline | date:'MM-dd' }}</span>
  88. </div>
  89. </li>
  90. }
  91. </ul>
  92. } @else {
  93. <div class="empty">抱歉,没有检索到哦</div>
  94. }
  95. </div>
  96. }
  97. </div>
  98. <div class="mode-switch" [attr.data-active]="ganttMode">
  99. <button [class.active]="ganttMode === 'project'" (click)="setGanttMode('project')">按项目</button>
  100. <button [class.active]="ganttMode === 'designer'" (click)="setGanttMode('designer')">设计师排班</button>
  101. </div>
  102. </div>
  103. <div #ganttChartRef class="gantt-chart"></div>
  104. </div>
  105. }
  106. @if (!showGanttView) {
  107. <div class="section-filters">
  108. <div class="search-box">
  109. <input type="search" class="input-search" placeholder="搜索项目/设计师/风格关键词" [(ngModel)]="searchTerm" (input)="onSearchChange()" (focus)="onSearchFocus()" (blur)="onSearchBlur()" />
  110. @if (showSuggestions) {
  111. <div class="suggestion-panel">
  112. @if (searchSuggestions.length > 0) {
  113. <ul>
  114. @for (suggest of searchSuggestions; track suggest.id) {
  115. <li (mousedown)="selectSuggestion(suggest)">
  116. <div class="line-1">
  117. <span class="name">{{ suggest.name }}</span>
  118. <span class="badge" [class.vip]="suggest.memberType==='vip'">{{ suggest.memberType==='vip' ? 'VIP' : '普通' }}</span>
  119. <span class="urgency" [class]="'u-' + suggest.urgency">{{ getUrgencyLabel(suggest.urgency) }}</span>
  120. </div>
  121. <div class="line-2">
  122. <span class="designer">{{ suggest.designerName || '未分配' }}</span>
  123. <span class="deadline">{{ suggest.deadline | date:'MM-dd' }}</span>
  124. </div>
  125. </li>
  126. }
  127. </ul>
  128. } @else {
  129. <div class="empty">抱歉,没有检索到哦</div>
  130. }
  131. </div>
  132. }
  133. </div>
  134. <select (change)="filterProjects($event)" class="custom-select">
  135. <option value="all">全部项目</option>
  136. <option value="soft">软装项目</option>
  137. <option value="hard">硬装项目</option>
  138. </select>
  139. <select (change)="filterByUrgency($event)" class="custom-select">
  140. <option value="all">全部紧急程度</option>
  141. <option value="high">高</option>
  142. <option value="medium">中</option>
  143. <option value="low">低</option>
  144. </select>
  145. <select (change)="onStatusChange($event)" class="custom-select">
  146. <option value="all">全部状态</option>
  147. <option value="progress">进行中</option>
  148. <option value="completed">已完成</option>
  149. <option value="overdue">已延期</option>
  150. <option value="dueSoon">临期(3天内)</option>
  151. <option value="pendingApproval">待确认</option>
  152. <option value="pendingAssignment">待分配</option>
  153. </select>
  154. <select (change)="onDesignerChange($event)" class="custom-select">
  155. <option value="all">全部设计师</option>
  156. @for (d of designers; track d) {
  157. <option [value]="d">{{ d }}</option>
  158. }
  159. </select>
  160. <select (change)="onMemberTypeChange($event)" class="custom-select">
  161. <option value="all">全部会员</option>
  162. <option value="vip">VIP会员</option>
  163. <option value="normal">普通会员</option>
  164. </select>
  165. <!-- 新增:四大板块筛选 -->
  166. <select [(ngModel)]="selectedCorePhase" (change)="onCorePhaseChange($event)" class="custom-select">
  167. <option value="all">全部板块</option>
  168. @for (core of corePhases; track core.id) {
  169. <option [value]="core.id">{{ core.name }}</option>
  170. }
  171. </select>
  172. <!-- 支持数百项目的下拉筛选 -->
  173. <select [(ngModel)]="selectedProjectId" (change)="selectProject()" class="custom-select project-selector">
  174. <option value="">选择项目</option>
  175. @for (project of projects; track project.id) {
  176. <option [value]="project.id">{{ project.name }}</option>
  177. }
  178. </select>
  179. <!-- 新增:时间窗快捷筛选按钮组 -->
  180. <div class="time-window-buttons">
  181. <button [class.active]="selectedTimeWindow === 'all'" (click)="filterByTimeWindow('all')">全部</button>
  182. <button [class.active]="selectedTimeWindow === 'today'" (click)="filterByTimeWindow('today')">今天到期</button>
  183. <button [class.active]="selectedTimeWindow === 'threeDays'" (click)="filterByTimeWindow('threeDays')">3天内</button>
  184. <button [class.active]="selectedTimeWindow === 'sevenDays'" (click)="filterByTimeWindow('sevenDays')">7天内</button>
  185. </div>
  186. </div>
  187. <!-- 项目看板 - 横向展开10个项目阶段 -->
  188. <div class="project-kanban">
  189. <!-- 新增:公共横向滚动容器,保证表头与表体同步滚动 -->
  190. <div class="kanban-scroll">
  191. <!-- 阶段标题 -->
  192. <div class="kanban-header">
  193. @for (core of corePhases; track core.id) {
  194. <div class="kanban-column-header">
  195. <h3>{{ core.name }}</h3>
  196. <span class="stage-count">{{ getProjectCountByCorePhase(core.id) }}</span>
  197. </div>
  198. }
  199. </div>
  200. <!-- 项目卡片 -->
  201. <div class="kanban-body">
  202. @for (core of corePhases; track core.id) {
  203. <div class="kanban-column">
  204. @for (project of getProjectsByCorePhase(core.id); track project.id) {
  205. <div class="project-card"
  206. (click)="viewProjectDetails(project.id)"
  207. [class.overdue]="project.isOverdue"
  208. [class.high-urgency]="project.urgency === 'high'"
  209. [class.due-soon]="project.dueSoon && !project.isOverdue">
  210. <div class="project-card-header">
  211. <h4 [routerLink]="['/team-leader/project-detail', project.id]" (click)="$event.stopPropagation()">{{ project.name }}</h4>
  212. <div class="right-badges">
  213. <span class="member-badge" [class.vip]="project.memberType === 'vip'">{{ project.memberType === 'vip' ? 'VIP' : '普通' }}</span>
  214. <span class="project-urgency" [class]="'urgency-' + project.urgency">{{ getUrgencyLabel(project.urgency) }}</span>
  215. </div>
  216. </div>
  217. <div class="project-card-content">
  218. <p>负责人: {{ project.designerName || '未分配' }}</p>
  219. <p class="deadline">{{ project.isOverdue ? '超期' + project.overdueDays + '天' : (project.dueSoon ? '临期: ' + (project.deadline | date:'MM-dd') : '截止: ' + (project.deadline | date:'MM-dd')) }}</p>
  220. </div>
  221. <div class="project-card-footer">
  222. <button (click)="viewProjectDetails(project.id); $event.stopPropagation()" class="btn-view">查看详情</button>
  223. @if (project.currentStage === 'pendingAssignment') {
  224. <button (click)="quickAssignProject(project.id); $event.stopPropagation()" class="btn-assign">分配</button>
  225. }
  226. <!-- 新增:质量评审快捷操作(保留,不影响四大板块分类) -->
  227. @if (project.currentStage === 'review' || project.currentStage === 'delivery') {
  228. <div class="inline-actions">
  229. <button class="btn-secondary" (click)="$event.stopPropagation(); reviewProjectQuality(project.id, 'excellent')">评为优秀</button>
  230. <button class="btn-secondary" (click)="$event.stopPropagation(); reviewProjectQuality(project.id, 'qualified')">评为合格</button>
  231. <button class="btn-secondary" (click)="$event.stopPropagation(); reviewProjectQuality(project.id, 'unqualified')">评为不合格</button>
  232. </div>
  233. }
  234. </div>
  235. </div>
  236. }
  237. @if (getProjectsByCorePhase(core.id).length === 0) {
  238. <div class="empty-column">
  239. <span class="empty-icon">📦</span>
  240. <p>暂无项目</p>
  241. </div>
  242. }
  243. </div>
  244. }
  245. </div>
  246. </div>
  247. </div>
  248. }
  249. </section>
  250. <!-- 待办任务优先级排序 -->
  251. <section class="todo-section">
  252. <div class="section-header">
  253. <h2>待办任务</h2>
  254. <!-- 新增:绩效与负载入口 -->
  255. <div class="section-actions">
  256. <button class="btn-link" (click)="viewPerformanceDetails()">查看绩效预警</button>
  257. <button class="btn-link" (click)="navigateToWorkloadCalendar()">打开负载日历</button>
  258. </div>
  259. </div>
  260. <div class="todo-list">
  261. @for (task of todoTasks; track task.id) {
  262. <div class="todo-item" [class.priority-high]="task.priority === 'high'" [class.priority-medium]="task.priority === 'medium'" [class.priority-low]="task.priority === 'low'">
  263. <div class="todo-header">
  264. <h3>{{ task.title }}</h3>
  265. <span class="task-priority">{{ getPriorityLabel(task.priority) }}</span>
  266. </div>
  267. <div class="todo-info">
  268. <p>{{ task.description }}</p>
  269. <p class="task-deadline">截止时间: {{ task.deadline | date:'yyyy-MM-dd HH:mm' }}</p>
  270. </div>
  271. <div class="todo-actions">
  272. <button (click)="navigateToTask(task)" class="btn-handle">处理任务</button>
  273. </div>
  274. </div>
  275. }
  276. </div>
  277. </section>
  278. <!-- 超期项目提醒 -->
  279. @if (showAlert && overdueProjects.length > 0) {
  280. <div class="overdue-alert">
  281. <div class="alert-content">
  282. <h3>⚠️ 超期项目提醒</h3>
  283. <ul>
  284. @for (project of overdueProjects.slice(0, 3); track $index) {
  285. <li>
  286. {{ project.name }} ({{ project.designerName }} 负责) - 超期{{ project.overdueDays }}天
  287. </li>
  288. }
  289. </ul>
  290. <div class="alert-actions">
  291. <button (click)="viewAllOverdueProjects()" class="btn-view-all">查看全部</button>
  292. <button (click)="closeAlert()" class="btn-close">关闭</button>
  293. </div>
  294. </div>
  295. </div>
  296. }
  297. @if (urgentPinnedProjects && urgentPinnedProjects.length > 0) {
  298. <div class="urgent-pinned">
  299. <div class="pinned-title">紧急任务固定区(超期 + 高紧急)</div>
  300. <div class="pinned-list">
  301. @for (p of urgentPinnedProjects.slice(0, 3); track $index) {
  302. <div class="pinned-item" (click)="filterByStatus('overdue')">
  303. <span class="dot dot-high"></span>
  304. <span class="name">{{ p.name }}</span>
  305. <span class="meta">{{ p.designerName || '未分配' }} · 超期{{ p.overdueDays }}天</span>
  306. </div>
  307. }
  308. @if (urgentPinnedProjects.length > 3) {
  309. <button class="btn-view-all" (click)="viewAllOverdueProjects()">更多…</button>
  310. }
  311. </div>
  312. </div>
  313. }
  314. </main>
  315. <!-- 员工详情面板 -->
  316. @if (showEmployeeDetailPanel && selectedEmployeeDetail) {
  317. <div class="employee-detail-overlay" (click)="closeEmployeeDetailPanel()">
  318. <div class="employee-detail-panel" (click)="$event.stopPropagation()">
  319. <!-- 面板头部 -->
  320. <div class="panel-header">
  321. <h3 class="panel-title">
  322. <svg class="icon-user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  323. <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
  324. <circle cx="12" cy="7" r="4"></circle>
  325. </svg>
  326. {{ selectedEmployeeDetail.name }} 详情
  327. </h3>
  328. <button class="btn-close" (click)="closeEmployeeDetailPanel()">
  329. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  330. <line x1="18" y1="6" x2="6" y2="18"></line>
  331. <line x1="6" y1="6" x2="18" y2="18"></line>
  332. </svg>
  333. </button>
  334. </div>
  335. <!-- 面板内容 -->
  336. <div class="panel-content">
  337. <!-- 负载概况栏 -->
  338. <div class="section workload-section">
  339. <div class="section-header">
  340. <svg class="section-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  341. <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
  342. <line x1="9" y1="9" x2="15" y2="9"></line>
  343. <line x1="9" y1="15" x2="15" y2="15"></line>
  344. </svg>
  345. <h4>负载概况</h4>
  346. </div>
  347. <div class="workload-info">
  348. <div class="workload-stat">
  349. <span class="stat-label">当前负责项目数:</span>
  350. <span class="stat-value" [class]="selectedEmployeeDetail.currentProjects >= 3 ? 'high-workload' : 'normal-workload'">
  351. {{ selectedEmployeeDetail.currentProjects }} 个
  352. </span>
  353. </div>
  354. @if (selectedEmployeeDetail.projectNames.length > 0) {
  355. <div class="project-list">
  356. <span class="project-label">核心项目:</span>
  357. <div class="project-tags">
  358. @for (projectName of selectedEmployeeDetail.projectNames; track $index) {
  359. <span class="project-tag">{{ projectName }}</span>
  360. }
  361. @if (selectedEmployeeDetail.currentProjects > selectedEmployeeDetail.projectNames.length) {
  362. <span class="project-tag more">+{{ selectedEmployeeDetail.currentProjects - selectedEmployeeDetail.projectNames.length }}</span>
  363. }
  364. </div>
  365. </div>
  366. }
  367. </div>
  368. </div>
  369. <!-- 请假明细栏 -->
  370. <div class="section leave-section">
  371. <div class="section-header">
  372. <svg class="section-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  373. <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
  374. <line x1="16" y1="2" x2="16" y2="6"></line>
  375. <line x1="8" y1="2" x2="8" y2="6"></line>
  376. <line x1="3" y1="10" x2="21" y2="10"></line>
  377. </svg>
  378. <h4>请假明细(未来7天)</h4>
  379. </div>
  380. <div class="leave-table">
  381. @if (selectedEmployeeDetail.leaveRecords.length > 0) {
  382. <table>
  383. <thead>
  384. <tr>
  385. <th>日期</th>
  386. <th>状态</th>
  387. <th>备注</th>
  388. </tr>
  389. </thead>
  390. <tbody>
  391. @for (record of selectedEmployeeDetail.leaveRecords; track record.id) {
  392. <tr [class]="record.isLeave ? 'leave-day' : 'work-day'">
  393. <td>{{ record.date | date:'M月d日' }}</td>
  394. <td>
  395. <span class="status-badge" [class]="record.isLeave ? 'leave' : 'work'">
  396. {{ record.isLeave ? '请假' : '正常' }}
  397. </span>
  398. </td>
  399. <td>{{ record.isLeave ? getLeaveTypeText(record.leaveType) : '-' }}</td>
  400. </tr>
  401. }
  402. </tbody>
  403. </table>
  404. } @else {
  405. <div class="no-leave">
  406. <svg class="no-data-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  407. <circle cx="12" cy="12" r="10"></circle>
  408. <path d="M8 14s1.5 2 4 2 4-2 4-2"></path>
  409. <line x1="9" y1="9" x2="9.01" y2="9"></line>
  410. <line x1="15" y1="9" x2="15.01" y2="9"></line>
  411. </svg>
  412. <p>未来7天无请假安排</p>
  413. </div>
  414. }
  415. </div>
  416. </div>
  417. <!-- 红色标记说明 -->
  418. <div class="section explanation-section">
  419. <div class="section-header">
  420. <svg class="section-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  421. <circle cx="12" cy="12" r="10"></circle>
  422. <line x1="12" y1="8" x2="12" y2="12"></line>
  423. <line x1="12" y1="16" x2="12.01" y2="16"></line>
  424. </svg>
  425. <h4>红色标记说明</h4>
  426. </div>
  427. <div class="explanation-content">
  428. <p class="explanation-text">{{ selectedEmployeeDetail.redMarkExplanation }}</p>
  429. </div>
  430. </div>
  431. </div>
  432. </div>
  433. </div>
  434. }