dashboard.html 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820
  1. <!-- 顶部导航栏 -->
  2. <nav class="top-navbar">
  3. <div class="navbar-left">
  4. <h2 class="navbar-title">设计组长工作台</h2>
  5. </div>
  6. <div class="navbar-right">
  7. <div class="date-display">
  8. {{ currentDate.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' }) }}
  9. </div>
  10. <div class="user-profile">
  11. <img [src]="currentUser.avatar" [alt]="currentUser.name + '头像'" class="user-avatar">
  12. <span class="user-name">{{ currentUser.name }}</span>
  13. <span class="user-role">{{ currentUser.roleName }}</span>
  14. </div>
  15. </div>
  16. </nav>
  17. <header class="dashboard-header">
  18. <!-- 核心数据指标卡片(扩展为6个) -->
  19. <div class="dashboard-metrics">
  20. <div class="metric-card" (click)="filterByStatus('overdue')">
  21. <div class="metric-icon warning">⚠️</div>
  22. <div class="metric-content">
  23. <div class="metric-count">{{ overdueProjects.length }}</div>
  24. <div class="metric-label">已延期项目</div>
  25. </div>
  26. </div>
  27. <div class="metric-card" (click)="filterByStatus('dueSoon')">
  28. <div class="metric-icon info">⏳</div>
  29. <div class="metric-content">
  30. <div class="metric-count">{{ dueSoonProjects.length }}</div>
  31. <div class="metric-label">临期项目(3天内)</div>
  32. </div>
  33. </div>
  34. <div class="metric-card" (click)="filterByStatus('pendingApproval')">
  35. <div class="metric-icon info">📋</div>
  36. <div class="metric-content">
  37. <div class="metric-count">{{ pendingApprovalProjects.length }}</div>
  38. <div class="metric-label">待组长确认项目</div>
  39. </div>
  40. </div>
  41. <div class="metric-card" (click)="filterByStatus('pendingAssignment')">
  42. <div class="metric-icon primary">🎯</div>
  43. <div class="metric-content">
  44. <div class="metric-count">{{ pendingAssignmentProjects.length }}</div>
  45. <div class="metric-label">待分配方案项目</div>
  46. </div>
  47. </div>
  48. <!-- 新增:超负荷设计师数量 -->
  49. <div class="metric-card">
  50. <div class="metric-icon danger">🔥</div>
  51. <div class="metric-content">
  52. <div class="metric-count">{{ overloadedDesignersCount }}</div>
  53. <div class="metric-label">超负荷设计师</div>
  54. </div>
  55. </div>
  56. <!-- 新增:平均负载率 -->
  57. <div class="metric-card">
  58. <div class="metric-icon success">📊</div>
  59. <div class="metric-content">
  60. <div class="metric-count">{{ averageWorkloadRate.toFixed(0) }}%</div>
  61. <div class="metric-label">平均负载率</div>
  62. </div>
  63. </div>
  64. </div>
  65. </header>
  66. <main class="dashboard-main">
  67. <!-- 项目监控大盘 -->
  68. <section class="monitoring-section">
  69. <div class="section-header">
  70. <h2>项目监控大盘</h2>
  71. <div class="section-actions">
  72. @if (selectedStatus !== 'all') {
  73. <button class="btn-link" (click)="resetStatusFilter()">返回全部项目</button>
  74. }
  75. <button class="btn-toggle-view" (click)="toggleView()">{{ showGanttView ? '返回看板' : '切换视图' }}</button>
  76. </div>
  77. </div>
  78. <!-- 工作量负载概览 -->
  79. <div class="workload-gantt-card">
  80. <div class="gantt-header">
  81. <h3>工作量负载概览</h3>
  82. <p class="gantt-subtitle">设计师每日工作状态一目了然</p>
  83. <div class="gantt-controls">
  84. <div class="scale-switch">
  85. <button [class.active]="workloadGanttScale === 'week'" (click)="setWorkloadGanttScale('week')">周视图</button>
  86. <button [class.active]="workloadGanttScale === 'month'" (click)="setWorkloadGanttScale('month')">月视图</button>
  87. </div>
  88. <div class="legend">
  89. <span class="legend-item"><span class="dot idle"></span>空闲</span>
  90. <span class="legend-item"><span class="dot busy"></span>忙碌</span>
  91. <span class="legend-item"><span class="dot overload"></span>超负荷</span>
  92. <span class="legend-item"><span class="dot leave"></span>请假</span>
  93. </div>
  94. </div>
  95. </div>
  96. <div class="gantt-container" #workloadGanttContainer></div>
  97. </div>
  98. @if (showGanttView) {
  99. <div class="gantt-card">
  100. <div class="gantt-header">
  101. <div class="title">项目负载时间轴</div>
  102. <div class="hint">
  103. 👤 设计师按负载由高到低排列 | 🎨 每个条形代表一个项目 | 🔴超期 🟠临期 🟢正常
  104. </div>
  105. <div class="scale-switch">
  106. <button [class.active]="ganttScale === 'week'" (click)="setGanttScale('week')">周</button>
  107. <button [class.active]="ganttScale === 'month'" (click)="setGanttScale('month')">月</button>
  108. </div>
  109. <div class="search-box">
  110. <input type="search" placeholder="搜索项目/设计师/风格关键词" [(ngModel)]="searchTerm" (input)="onSearchChange()" (focus)="onSearchFocus()" (blur)="onSearchBlur()" />
  111. @if (showSuggestions) {
  112. <div class="suggestion-panel">
  113. @if (searchSuggestions.length > 0) {
  114. <ul>
  115. @for (suggest of searchSuggestions; track suggest.id) {
  116. <li (mousedown)="selectSuggestion(suggest)">
  117. <div class="line-1">
  118. <span class="name">{{ suggest.name }}</span>
  119. <span class="badge" [class.vip]="suggest.memberType==='vip'">{{ suggest.memberType==='vip' ? 'VIP' : '普通' }}</span>
  120. <span class="urgency" [class]="'u-' + suggest.urgency">{{ getUrgencyLabel(suggest.urgency) }}</span>
  121. </div>
  122. <div class="line-2">
  123. <span class="designer">{{ suggest.designerName || '未分配' }}</span>
  124. <span class="deadline">{{ suggest.deadline | date:'MM-dd' }}</span>
  125. </div>
  126. </li>
  127. }
  128. </ul>
  129. } @else {
  130. <div class="empty">抱歉,没有检索到哦</div>
  131. }
  132. </div>
  133. }
  134. </div>
  135. </div>
  136. <div #ganttChartRef class="gantt-chart"></div>
  137. </div>
  138. }
  139. @if (!showGanttView) {
  140. <div class="section-filters">
  141. <div class="search-box">
  142. <input type="search" class="input-search" placeholder="搜索项目/设计师/风格关键词" [(ngModel)]="searchTerm" (input)="onSearchChange()" (focus)="onSearchFocus()" (blur)="onSearchBlur()" />
  143. @if (showSuggestions) {
  144. <div class="suggestion-panel">
  145. @if (searchSuggestions.length > 0) {
  146. <ul>
  147. @for (suggest of searchSuggestions; track suggest.id) {
  148. <li (mousedown)="selectSuggestion(suggest)">
  149. <div class="line-1">
  150. <span class="name">{{ suggest.name }}</span>
  151. <span class="badge" [class.vip]="suggest.memberType==='vip'">{{ suggest.memberType==='vip' ? 'VIP' : '普通' }}</span>
  152. <span class="urgency" [class]="'u-' + suggest.urgency">{{ getUrgencyLabel(suggest.urgency) }}</span>
  153. </div>
  154. <div class="line-2">
  155. <span class="designer">{{ suggest.designerName || '未分配' }}</span>
  156. <span class="deadline">{{ suggest.deadline | date:'MM-dd' }}</span>
  157. </div>
  158. </li>
  159. }
  160. </ul>
  161. } @else {
  162. <div class="empty">抱歉,没有检索到哦</div>
  163. }
  164. </div>
  165. }
  166. </div>
  167. <select (change)="filterProjects($event)" class="custom-select">
  168. <option value="all">全部项目</option>
  169. <option value="soft">软装项目</option>
  170. <option value="hard">硬装项目</option>
  171. </select>
  172. <select (change)="filterByUrgency($event)" class="custom-select">
  173. <option value="all">全部紧急程度</option>
  174. <option value="high">高</option>
  175. <option value="medium">中</option>
  176. <option value="low">低</option>
  177. </select>
  178. <select (change)="onStatusChange($event)" class="custom-select">
  179. <option value="all">全部状态</option>
  180. <option value="progress">进行中</option>
  181. <option value="completed">已完成</option>
  182. <option value="overdue">已延期</option>
  183. <option value="dueSoon">临期(3天内)</option>
  184. <option value="pendingApproval">待确认</option>
  185. <option value="pendingAssignment">待分配</option>
  186. </select>
  187. <select (change)="onDesignerChange($event)" class="custom-select">
  188. <option value="all">全部设计师</option>
  189. @for (d of designers; track d) {
  190. <option [value]="d">{{ d }}</option>
  191. }
  192. </select>
  193. <select (change)="onMemberTypeChange($event)" class="custom-select">
  194. <option value="all">全部会员</option>
  195. <option value="vip">VIP会员</option>
  196. <option value="normal">普通会员</option>
  197. </select>
  198. <!-- 新增:四大板块筛选 -->
  199. <select [(ngModel)]="selectedCorePhase" (change)="onCorePhaseChange($event)" class="custom-select">
  200. <option value="all">全部板块</option>
  201. @for (core of corePhases; track core.id) {
  202. <option [value]="core.id">{{ core.name }}</option>
  203. }
  204. </select>
  205. <!-- 支持数百项目的下拉筛选 -->
  206. <select [(ngModel)]="selectedProjectId" (change)="selectProject()" class="custom-select project-selector">
  207. <option value="">选择项目</option>
  208. @for (project of projects; track project.id) {
  209. <option [value]="project.id">{{ project.name }}</option>
  210. }
  211. </select>
  212. <!-- 新增:时间窗快捷筛选按钮组 -->
  213. <div class="time-window-buttons">
  214. <button [class.active]="selectedTimeWindow === 'all'" (click)="filterByTimeWindow('all')">全部</button>
  215. <button [class.active]="selectedTimeWindow === 'today'" (click)="filterByTimeWindow('today')">今天到期</button>
  216. <button [class.active]="selectedTimeWindow === 'threeDays'" (click)="filterByTimeWindow('threeDays')">3天内</button>
  217. <button [class.active]="selectedTimeWindow === 'sevenDays'" (click)="filterByTimeWindow('sevenDays')">7天内</button>
  218. </div>
  219. </div>
  220. <!-- 项目看板 - 横向展开10个项目阶段 -->
  221. <div class="project-kanban">
  222. <!-- 新增:公共横向滚动容器,保证表头与表体同步滚动 -->
  223. <div class="kanban-scroll">
  224. <!-- 阶段标题 -->
  225. <div class="kanban-header">
  226. @for (core of corePhases; track core.id) {
  227. <div class="kanban-column-header">
  228. <h3>{{ core.name }}</h3>
  229. <span class="stage-count">{{ getProjectCountByCorePhase(core.id) }}</span>
  230. </div>
  231. }
  232. </div>
  233. <!-- 项目卡片 -->
  234. <div class="kanban-body">
  235. @for (core of corePhases; track core.id) {
  236. <div class="kanban-column">
  237. @for (project of getProjectsByCorePhase(core.id); track project.id) {
  238. <div class="project-card"
  239. (click)="viewProjectDetails(project.id)"
  240. [class.overdue]="project.isOverdue"
  241. [class.high-urgency]="project.urgency === 'high'"
  242. [class.due-soon]="project.dueSoon && !project.isOverdue"
  243. [class.pending-approval]="isPendingApproval(project)">
  244. <!-- 待审批徽章 -->
  245. @if (isPendingApproval(project)) {
  246. <div class="approval-badge">
  247. <span class="badge-icon">📋</span>
  248. <span class="badge-text">待审批</span>
  249. </div>
  250. }
  251. <div class="project-card-header">
  252. <h4 (click)="viewProjectDetails(project.id); $event.stopPropagation()" style="cursor: pointer;">{{ project.name }}</h4>
  253. <div class="right-badges">
  254. <span class="member-badge" [class.vip]="project.memberType === 'vip'">{{ project.memberType === 'vip' ? 'VIP' : '普通' }}</span>
  255. <span class="project-urgency" [class]="'urgency-' + project.urgency">{{ getUrgencyLabel(project.urgency) }}</span>
  256. </div>
  257. </div>
  258. <div class="project-card-content">
  259. <p>负责人: {{ project.designerName || '未分配' }}</p>
  260. <p class="deadline">{{ project.isOverdue ? '超期' + project.overdueDays + '天' : (project.dueSoon ? '临期: ' + (project.deadline | date:'MM-dd') : '截止: ' + (project.deadline | date:'MM-dd')) }}</p>
  261. </div>
  262. <div class="project-card-footer">
  263. <button (click)="viewProjectDetails(project.id); $event.stopPropagation()" class="btn-view">查看详情</button>
  264. @if (project.currentStage === 'pendingAssignment') {
  265. <button (click)="openSmartMatch(project); $event.stopPropagation()" class="btn-smart">🤖 智能推荐</button>
  266. <button (click)="quickAssignProject(project.id); $event.stopPropagation()" class="btn-assign">手动分配</button>
  267. }
  268. <!-- 新增:质量评审快捷操作(保留,不影响四大板块分类) -->
  269. @if (project.currentStage === 'review' || project.currentStage === 'delivery') {
  270. <div class="inline-actions">
  271. <button class="btn-secondary" (click)="$event.stopPropagation(); reviewProjectQuality(project.id, 'excellent')">评为优秀</button>
  272. <button class="btn-secondary" (click)="$event.stopPropagation(); reviewProjectQuality(project.id, 'qualified')">评为合格</button>
  273. <button class="btn-secondary" (click)="$event.stopPropagation(); reviewProjectQuality(project.id, 'unqualified')">评为不合格</button>
  274. </div>
  275. }
  276. </div>
  277. </div>
  278. }
  279. @if (getProjectsByCorePhase(core.id).length === 0) {
  280. <div class="empty-column">
  281. <span class="empty-icon">📦</span>
  282. <p>暂无项目</p>
  283. </div>
  284. }
  285. </div>
  286. }
  287. </div>
  288. </div>
  289. </div>
  290. }
  291. </section>
  292. <!-- 待办任务优先级排序 -->
  293. <section class="todo-section">
  294. <div class="section-header">
  295. <h2>待办任务</h2>
  296. <!-- 新增:绩效与负载入口 -->
  297. <div class="section-actions">
  298. <button class="btn-link" (click)="viewPerformanceDetails()">查看绩效预警</button>
  299. <button class="btn-link" (click)="navigateToWorkloadCalendar()">打开负载日历</button>
  300. </div>
  301. </div>
  302. <div class="todo-list">
  303. @for (task of todoTasks; track task.id) {
  304. <div class="todo-item" [class.priority-high]="task.priority === 'high'" [class.priority-medium]="task.priority === 'medium'" [class.priority-low]="task.priority === 'low'">
  305. <div class="todo-header">
  306. <h3>{{ task.title }}</h3>
  307. <span class="task-priority">{{ getPriorityLabel(task.priority) }}</span>
  308. </div>
  309. <div class="todo-info">
  310. <p>{{ task.description }}</p>
  311. <p class="task-deadline">截止时间: {{ task.deadline | date:'yyyy-MM-dd HH:mm' }}</p>
  312. </div>
  313. <div class="todo-actions">
  314. <button (click)="navigateToTask(task)" class="btn-handle">处理任务</button>
  315. </div>
  316. </div>
  317. }
  318. </div>
  319. </section>
  320. <!-- 超期项目提醒 -->
  321. @if (showAlert && overdueProjects.length > 0) {
  322. <div class="overdue-alert">
  323. <div class="alert-content">
  324. <h3>⚠️ 超期项目提醒</h3>
  325. <ul>
  326. @for (project of overdueProjects.slice(0, 3); track $index) {
  327. <li>
  328. {{ project.name }} ({{ project.designerName }} 负责) - 超期{{ project.overdueDays }}天
  329. </li>
  330. }
  331. </ul>
  332. <div class="alert-actions">
  333. <button (click)="viewAllOverdueProjects()" class="btn-view-all">查看全部</button>
  334. <button (click)="closeAlert()" class="btn-close">关闭</button>
  335. </div>
  336. </div>
  337. </div>
  338. }
  339. @if (urgentPinnedProjects && urgentPinnedProjects.length > 0) {
  340. <div class="urgent-pinned">
  341. <div class="pinned-title">紧急任务固定区(超期 + 高紧急)</div>
  342. <div class="pinned-list">
  343. @for (p of urgentPinnedProjects.slice(0, 3); track $index) {
  344. <div class="pinned-item" (click)="filterByStatus('overdue')">
  345. <span class="dot dot-high"></span>
  346. <span class="name">{{ p.name }}</span>
  347. <span class="meta">{{ p.designerName || '未分配' }} · 超期{{ p.overdueDays }}天</span>
  348. </div>
  349. }
  350. @if (urgentPinnedProjects.length > 3) {
  351. <button class="btn-view-all" (click)="viewAllOverdueProjects()">更多…</button>
  352. }
  353. </div>
  354. </div>
  355. }
  356. </main>
  357. <!-- 员工详情面板 -->
  358. @if (showEmployeeDetailPanel && selectedEmployeeDetail) {
  359. <div class="employee-detail-overlay" (click)="closeEmployeeDetailPanel()">
  360. <div class="employee-detail-panel" (click)="$event.stopPropagation()">
  361. <!-- 面板头部 -->
  362. <div class="panel-header">
  363. <h3 class="panel-title">
  364. <svg class="icon-user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  365. <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
  366. <circle cx="12" cy="7" r="4"></circle>
  367. </svg>
  368. {{ selectedEmployeeDetail.name }} 详情
  369. </h3>
  370. <button class="btn-close" (click)="closeEmployeeDetailPanel()">
  371. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  372. <line x1="18" y1="6" x2="6" y2="18"></line>
  373. <line x1="6" y1="6" x2="18" y2="18"></line>
  374. </svg>
  375. </button>
  376. </div>
  377. <!-- 面板内容 -->
  378. <div class="panel-content">
  379. <!-- 负载概况栏 -->
  380. <div class="section workload-section">
  381. <div class="section-header">
  382. <svg class="section-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  383. <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
  384. <line x1="9" y1="9" x2="15" y2="9"></line>
  385. <line x1="9" y1="15" x2="15" y2="15"></line>
  386. </svg>
  387. <h4>负载概况</h4>
  388. </div>
  389. <div class="workload-info">
  390. <div class="workload-stat">
  391. <span class="stat-label">当前负责项目数:</span>
  392. <span class="stat-value" [class]="selectedEmployeeDetail.currentProjects >= 3 ? 'high-workload' : 'normal-workload'">
  393. {{ selectedEmployeeDetail.currentProjects }} 个
  394. </span>
  395. </div>
  396. @if (selectedEmployeeDetail.projectData.length > 0) {
  397. <div class="project-list">
  398. <span class="project-label">核心项目:</span>
  399. <div class="project-tags">
  400. @for (project of selectedEmployeeDetail.projectData; track project.id) {
  401. <span class="project-tag clickable"
  402. (click)="navigateToProjectFromPanel(project.id)"
  403. title="点击查看项目详情">
  404. {{ project.name }}
  405. <svg class="icon-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  406. <path d="M7 17L17 7M17 7H7M17 7V17"/>
  407. </svg>
  408. </span>
  409. }
  410. @if (selectedEmployeeDetail.currentProjects > selectedEmployeeDetail.projectData.length) {
  411. <span class="project-tag more">+{{ selectedEmployeeDetail.currentProjects - selectedEmployeeDetail.projectData.length }}</span>
  412. }
  413. </div>
  414. </div>
  415. }
  416. </div>
  417. </div>
  418. <!-- 负载详细日历 -->
  419. <div class="section calendar-section">
  420. <div class="section-header">
  421. <svg class="section-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  422. <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
  423. <line x1="16" y1="2" x2="16" y2="6"></line>
  424. <line x1="8" y1="2" x2="8" y2="6"></line>
  425. <line x1="3" y1="10" x2="21" y2="10"></line>
  426. </svg>
  427. <h4>负载详细日历</h4>
  428. </div>
  429. @if (selectedEmployeeDetail.calendarData) {
  430. <div class="employee-calendar">
  431. <!-- 月份标题 -->
  432. <div class="calendar-month-header">
  433. <span class="month-title">
  434. {{ selectedEmployeeDetail.calendarData.currentMonth | date:'yyyy年M月' }}
  435. </span>
  436. </div>
  437. <!-- 星期标题 -->
  438. <div class="calendar-weekdays">
  439. <div class="weekday">日</div>
  440. <div class="weekday">一</div>
  441. <div class="weekday">二</div>
  442. <div class="weekday">三</div>
  443. <div class="weekday">四</div>
  444. <div class="weekday">五</div>
  445. <div class="weekday">六</div>
  446. </div>
  447. <!-- 日历网格 -->
  448. <div class="calendar-grid">
  449. @for (day of selectedEmployeeDetail.calendarData.days; track day.date.getTime()) {
  450. <div class="calendar-day"
  451. [class.today]="day.isToday"
  452. [class.other-month]="!day.isCurrentMonth"
  453. [class.has-projects]="day.projectCount > 0"
  454. [class.clickable]="day.projectCount > 0 && day.isCurrentMonth"
  455. (click)="onCalendarDayClick(day)">
  456. <div class="day-number">{{ day.date.getDate() }}</div>
  457. @if (day.projectCount > 0) {
  458. <div class="day-badge" [class.high-load]="day.projectCount >= 2">
  459. {{ day.projectCount }}个项目
  460. </div>
  461. }
  462. </div>
  463. }
  464. </div>
  465. <!-- 图例 -->
  466. <div class="calendar-legend">
  467. <div class="legend-item">
  468. <span class="legend-dot today-dot"></span>
  469. <span class="legend-text">今天</span>
  470. </div>
  471. <div class="legend-item">
  472. <span class="legend-dot project-dot"></span>
  473. <span class="legend-text">有项目</span>
  474. </div>
  475. <div class="legend-item">
  476. <span class="legend-dot high-dot"></span>
  477. <span class="legend-text">高负载</span>
  478. </div>
  479. </div>
  480. </div>
  481. }
  482. </div>
  483. <!-- 请假明细栏 -->
  484. <div class="section leave-section">
  485. <div class="section-header">
  486. <svg class="section-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  487. <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
  488. <line x1="16" y1="2" x2="16" y2="6"></line>
  489. <line x1="8" y1="2" x2="8" y2="6"></line>
  490. <line x1="3" y1="10" x2="21" y2="10"></line>
  491. </svg>
  492. <h4>请假明细(未来7天)</h4>
  493. </div>
  494. <div class="leave-table">
  495. @if (selectedEmployeeDetail.leaveRecords.length > 0) {
  496. <table>
  497. <thead>
  498. <tr>
  499. <th>日期</th>
  500. <th>状态</th>
  501. <th>备注</th>
  502. </tr>
  503. </thead>
  504. <tbody>
  505. @for (record of selectedEmployeeDetail.leaveRecords; track record.id) {
  506. <tr [class]="record.isLeave ? 'leave-day' : 'work-day'">
  507. <td>{{ record.date | date:'M月d日' }}</td>
  508. <td>
  509. <span class="status-badge" [class]="record.isLeave ? 'leave' : 'work'">
  510. {{ record.isLeave ? '请假' : '正常' }}
  511. </span>
  512. </td>
  513. <td>{{ record.isLeave ? getLeaveTypeText(record.leaveType) : '-' }}</td>
  514. </tr>
  515. }
  516. </tbody>
  517. </table>
  518. } @else {
  519. <div class="no-leave">
  520. <svg class="no-data-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  521. <circle cx="12" cy="12" r="10"></circle>
  522. <path d="M8 14s1.5 2 4 2 4-2 4-2"></path>
  523. <line x1="9" y1="9" x2="9.01" y2="9"></line>
  524. <line x1="15" y1="9" x2="15.01" y2="9"></line>
  525. </svg>
  526. <p>未来7天无请假安排</p>
  527. </div>
  528. }
  529. </div>
  530. </div>
  531. <!-- 红色标记说明 -->
  532. <div class="section explanation-section">
  533. <div class="section-header">
  534. <svg class="section-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  535. <circle cx="12" cy="12" r="10"></circle>
  536. <line x1="12" y1="8" x2="12" y2="12"></line>
  537. <line x1="12" y1="16" x2="12.01" y2="16"></line>
  538. </svg>
  539. <h4>红色标记说明</h4>
  540. </div>
  541. <div class="explanation-content">
  542. <p class="explanation-text">{{ selectedEmployeeDetail.redMarkExplanation }}</p>
  543. </div>
  544. </div>
  545. <!-- 新增:能力问卷 -->
  546. <div class="section survey-section">
  547. <div class="section-header">
  548. <svg class="section-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  549. <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"/>
  550. </svg>
  551. <h4>能力问卷</h4>
  552. <button
  553. class="btn-refresh-survey"
  554. (click)="refreshEmployeeSurvey()"
  555. [disabled]="refreshingSurvey"
  556. title="刷新问卷状态">
  557. <svg viewBox="0 0 24 24" width="16" height="16" [class.rotating]="refreshingSurvey">
  558. <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"/>
  559. </svg>
  560. </button>
  561. </div>
  562. @if (selectedEmployeeDetail.surveyCompleted && selectedEmployeeDetail.surveyData) {
  563. <div class="survey-content">
  564. <div class="survey-status completed">
  565. <svg viewBox="0 0 24 24" width="20" height="20" fill="#34c759">
  566. <path d="M9,20.42L2.79,14.21L5.62,11.38L9,14.77L18.88,4.88L21.71,7.71L9,20.42Z"/>
  567. </svg>
  568. <span>已完成问卷</span>
  569. <span class="survey-time">
  570. {{ selectedEmployeeDetail.surveyData.createdAt | date:'yyyy-MM-dd HH:mm' }}
  571. </span>
  572. </div>
  573. <!-- 能力画像摘要 -->
  574. @if (!showFullSurvey) {
  575. <div class="capability-summary">
  576. <h5>您的能力画像</h5>
  577. @if (getCapabilitySummary(selectedEmployeeDetail.surveyData.answers); as summary) {
  578. <div class="summary-grid">
  579. <div class="summary-item">
  580. <span class="label">擅长风格:</span>
  581. <span class="value">{{ summary.styles }}</span>
  582. </div>
  583. <div class="summary-item">
  584. <span class="label">擅长空间:</span>
  585. <span class="value">{{ summary.spaces }}</span>
  586. </div>
  587. <div class="summary-item">
  588. <span class="label">技术优势:</span>
  589. <span class="value">{{ summary.advantages }}</span>
  590. </div>
  591. <div class="summary-item">
  592. <span class="label">项目难度:</span>
  593. <span class="value">{{ summary.difficulty }}</span>
  594. </div>
  595. <div class="summary-item">
  596. <span class="label">周承接量:</span>
  597. <span class="value">{{ summary.capacity }}</span>
  598. </div>
  599. <div class="summary-item">
  600. <span class="label">紧急订单:</span>
  601. <span class="value">
  602. {{ summary.urgent }}
  603. @if (summary.urgentLimit) {
  604. <span class="limit-hint">(每月不超过{{summary.urgentLimit}}次)</span>
  605. }
  606. </span>
  607. </div>
  608. <div class="summary-item">
  609. <span class="label">进度同步:</span>
  610. <span class="value">{{ summary.feedback }}</span>
  611. </div>
  612. <div class="summary-item">
  613. <span class="label">沟通方式:</span>
  614. <span class="value">{{ summary.communication }}</span>
  615. </div>
  616. </div>
  617. }
  618. <button class="btn-view-full" (click)="toggleSurveyDisplay()">
  619. <svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
  620. <path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
  621. </svg>
  622. 查看完整问卷(共 {{ selectedEmployeeDetail.surveyData.answers.length }} 道题)
  623. </button>
  624. </div>
  625. }
  626. <!-- 完整问卷答案 -->
  627. @if (showFullSurvey) {
  628. <div class="survey-answers">
  629. <h5>完整问卷答案(共 {{ selectedEmployeeDetail.surveyData.answers.length }} 道题):</h5>
  630. @for (answer of selectedEmployeeDetail.surveyData.answers; track $index) {
  631. <div class="answer-item">
  632. <div class="question-text">
  633. <strong>Q{{$index + 1}}:</strong> {{ answer.question }}
  634. </div>
  635. <div class="answer-text">
  636. @if (!answer.answer) {
  637. <span class="answer-tag empty">未填写(选填)</span>
  638. } @else if (answer.type === 'single' || answer.type === 'text' || answer.type === 'textarea' || answer.type === 'number') {
  639. <span class="answer-tag single">{{ answer.answer }}</span>
  640. } @else if (answer.type === 'multiple') {
  641. @if (Array.isArray(answer.answer)) {
  642. @for (opt of answer.answer; track opt) {
  643. <span class="answer-tag multiple">{{ opt }}</span>
  644. }
  645. } @else {
  646. <span class="answer-tag single">{{ answer.answer }}</span>
  647. }
  648. } @else if (answer.type === 'scale') {
  649. <div class="answer-scale">
  650. <div class="scale-bar">
  651. <div class="scale-fill" [style.width.%]="(answer.answer / 10) * 100">
  652. <span>{{ answer.answer }} / 10</span>
  653. </div>
  654. </div>
  655. </div>
  656. } @else {
  657. <span class="answer-tag single">{{ answer.answer }}</span>
  658. }
  659. </div>
  660. </div>
  661. }
  662. <button class="btn-collapse" (click)="toggleSurveyDisplay()">
  663. <svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
  664. <path d="M19 13H5v-2h14v2z"/>
  665. </svg>
  666. 收起详情
  667. </button>
  668. </div>
  669. }
  670. </div>
  671. } @else {
  672. <div class="survey-empty">
  673. <svg class="no-data-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  674. <circle cx="12" cy="12" r="10"></circle>
  675. <path d="M8 12h8M12 8v8"/>
  676. </svg>
  677. <p>该员工尚未完成能力问卷</p>
  678. </div>
  679. }
  680. </div>
  681. </div>
  682. </div>
  683. </div>
  684. }
  685. <!-- 日历项目列表弹窗 -->
  686. @if (showCalendarProjectList) {
  687. <div class="calendar-project-modal-overlay" (click)="closeCalendarProjectList()">
  688. <div class="calendar-project-modal" (click)="$event.stopPropagation()">
  689. <div class="modal-header">
  690. <h3>
  691. <svg class="header-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  692. <path d="M9 11l3 3L22 4"></path>
  693. <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
  694. </svg>
  695. {{ selectedDate | date:'M月d日' }} 的项目
  696. </h3>
  697. <button class="btn-close" (click)="closeCalendarProjectList()">
  698. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  699. <line x1="18" y1="6" x2="6" y2="18"></line>
  700. <line x1="6" y1="6" x2="18" y2="18"></line>
  701. </svg>
  702. </button>
  703. </div>
  704. <div class="modal-body">
  705. <div class="project-count-info">
  706. 共 <strong>{{ selectedDayProjects.length }}</strong> 个项目
  707. </div>
  708. <div class="project-list">
  709. @for (project of selectedDayProjects; track project.id) {
  710. <div class="project-item" (click)="navigateToProjectFromPanel(project.id); closeCalendarProjectList()">
  711. <div class="project-info">
  712. <svg class="project-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  713. <path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path>
  714. <path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path>
  715. </svg>
  716. <div class="project-details">
  717. <h4 class="project-name">{{ project.name }}</h4>
  718. @if (project.deadline) {
  719. <p class="project-deadline">
  720. 截止日期: {{ project.deadline | date:'yyyy-MM-dd' }}
  721. </p>
  722. }
  723. </div>
  724. </div>
  725. <svg class="arrow-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  726. <path d="M5 12h14M12 5l7 7-7 7"/>
  727. </svg>
  728. </div>
  729. }
  730. </div>
  731. </div>
  732. </div>
  733. </div>
  734. }
  735. <!-- 智能推荐弹窗 -->
  736. @if (showSmartMatch) {
  737. <div class="smart-match-modal">
  738. <div class="modal-backdrop" (click)="closeSmartMatch()"></div>
  739. <div class="modal-content">
  740. <div class="modal-header">
  741. <h3>🤖 智能推荐设计师</h3>
  742. <button class="btn-close" (click)="closeSmartMatch()">×</button>
  743. </div>
  744. @if (selectedProject) {
  745. <div class="project-info">
  746. <h4>{{ selectedProject.name }}</h4>
  747. <div class="tags">
  748. <span class="tag">{{ selectedProject.type === 'hard' ? '硬装' : '软装' }}</span>
  749. <span class="tag">{{ selectedProject.memberType === 'vip' ? 'VIP' : '普通' }}</span>
  750. <span class="tag urgency u-{{ selectedProject.urgency }}">
  751. {{ getUrgencyLabel(selectedProject.urgency) }}
  752. </span>
  753. </div>
  754. </div>
  755. }
  756. <div class="recommendations-list">
  757. @for (rec of recommendations; track rec.designer.id; let i = $index) {
  758. <div class="rec-card">
  759. <div class="rank" [class.gold]="i===0" [class.silver]="i===1" [class.bronze]="i===2">
  760. {{ i + 1 }}
  761. </div>
  762. <div class="designer-info">
  763. <h4>{{ rec.designer.name }}</h4>
  764. <div class="match-score-bar">
  765. <div class="score-fill" [style.width.%]="rec.matchScore">
  766. <span>{{ rec.matchScore }}分</span>
  767. </div>
  768. </div>
  769. </div>
  770. <div class="details">
  771. <p><strong>擅长:</strong>{{ rec.designer.tags.expertise.styles.join('、') || '暂无标签' }}</p>
  772. <p><strong>负载:</strong>{{ rec.loadRate.toFixed(0) }}% ({{ rec.currentProjects }}个项目)</p>
  773. <p><strong>评分:</strong>⭐ {{ rec.designer.tags.history.avgRating || '暂无' }}</p>
  774. <p class="reason"><strong>推荐理由:</strong>{{ rec.reason }}</p>
  775. </div>
  776. <button class="btn-assign" (click)="assignToDesigner(rec.designer.id)">
  777. 分配给TA
  778. </button>
  779. </div>
  780. }
  781. @if (recommendations.length === 0) {
  782. <div class="empty">
  783. <p>未找到合适的设计师</p>
  784. <p>您可以手动分配或调整项目参数</p>
  785. </div>
  786. }
  787. </div>
  788. </div>
  789. </div>
  790. }