dashboard.html 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881
  1. <!-- 欢迎区域 -->
  2. <section class="welcome-section">
  3. <div class="welcome-header">
  4. <div>
  5. <h2>您好,{{currentUser?.name}} 👋</h2>
  6. <p>今天是 {{ currentDate.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' }) }},祝您工作顺利!</p>
  7. </div>
  8. <button class="attendance-view-btn" (click)="viewAttendance()" title="查看人员考勤">
  9. <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  10. <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
  11. <line x1="16" y1="2" x2="16" y2="6"></line>
  12. <line x1="8" y1="2" x2="8" y2="6"></line>
  13. <line x1="3" y1="10" x2="21" y2="10"></line>
  14. </svg>
  15. 查看考勤
  16. </button>
  17. </div>
  18. </section>
  19. <!-- 数据看板 -->
  20. <section class="stats-dashboard">
  21. <div class="stats-grid">
  22. <!-- 项目总数 -->
  23. <div class="stat-card" (click)="handleTotalProjectsClick()" title="点击查看所有项目">
  24. <div class="stat-icon primary">
  25. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  26. <path d="M3 3h7v7H3z"></path>
  27. <path d="M14 3h7v7h-7z"></path>
  28. <path d="M14 14h7v7h-7z"></path>
  29. <path d="M3 14h7v7H3z"></path>
  30. </svg>
  31. </div>
  32. <div class="stat-content">
  33. <div class="stat-value">{{ stats.totalProjects() }}</div>
  34. <div class="stat-label">项目总数</div>
  35. </div>
  36. </div>
  37. <!-- 新咨询数 - 已隐藏 -->
  38. <!-- <div class="stat-card" (click)="handleNewConsultationsClick()" title="点击查看新咨询详情">
  39. <div class="stat-icon secondary">
  40. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  41. <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
  42. </svg>
  43. </div>
  44. <div class="stat-content">
  45. <div class="stat-value">{{ stats.newConsultations() }}</div>
  46. <div class="stat-label">新咨询数</div>
  47. </div>
  48. </div> -->
  49. <!-- 待分配项目数 -->
  50. <div class="stat-card" (click)="handlePendingAssignmentsClick()" title="点击查看待分配项目详情">
  51. <div class="stat-icon warning">
  52. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  53. <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
  54. <polyline points="22 4 12 14.01 9 11.01"></polyline>
  55. </svg>
  56. </div>
  57. <div class="stat-content">
  58. <div class="stat-value">{{ stats.pendingAssignments() }}</div>
  59. <div class="stat-label">待分配项目数</div>
  60. </div>
  61. </div>
  62. <!-- 异常项目 - 已隐藏 -->
  63. <!-- <div class="stat-card" (click)="handleExceptionProjectsClick()" title="点击查看异常项目详情">
  64. <div class="stat-icon danger">
  65. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  66. <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
  67. <line x1="12" y1="9" x2="12" y2="13"></line>
  68. <line x1="12" y1="17" x2="12.01" y2="17"></line>
  69. </svg>
  70. </div>
  71. <div class="stat-content">
  72. <div class="stat-value">{{ stats.exceptionProjects() }}</div>
  73. <div class="stat-label">异常项目</div>
  74. </div>
  75. </div> -->
  76. <!-- 售后服务 - 已隐藏 -->
  77. <!-- <div class="stat-card" (click)="handleAfterSalesClick()" title="点击查看售后服务详情">
  78. <div class="stat-icon success">
  79. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  80. <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"></path>
  81. </svg>
  82. </div>
  83. <div class="stat-content">
  84. <div class="stat-value">{{ stats.afterSalesCount() }}</div>
  85. <div class="stat-label">售后服务</div>
  86. </div>
  87. </div> -->
  88. </div>
  89. </section>
  90. <!-- 新客户触达 与 老客户回访 - 暂时隐藏,等待后续功能开发 -->
  91. <!-- <section class="crm-queues">
  92. <div class="crm-grid">
  93. <div class="crm-card">
  94. <div class="crm-header">
  95. <div class="crm-title-section">
  96. <h3>新客户触达</h3>
  97. <div class="crm-stats">
  98. <div class="stat-item">
  99. <span class="stat-number">0</span>
  100. <span class="stat-label">待触达</span>
  101. </div>
  102. <div class="stat-item">
  103. <span class="stat-number success">0%</span>
  104. <span class="stat-label">转化率</span>
  105. </div>
  106. </div>
  107. </div>
  108. <a class="view-all-link" (click)="goToConsultationList()">查看全部</a>
  109. </div>
  110. <div class="crm-list">
  111. <div class="empty-state small">暂无待触达客户</div>
  112. </div>
  113. </div>
  114. <div class="crm-card">
  115. <div class="crm-header">
  116. <div class="crm-title-section">
  117. <h3>老客户回访</h3>
  118. <div class="crm-stats">
  119. <div class="stat-item">
  120. <span class="stat-number">0</span>
  121. <span class="stat-label">待回访</span>
  122. </div>
  123. <div class="stat-item">
  124. <span class="stat-number warning">0%</span>
  125. <span class="stat-label">留存率</span>
  126. </div>
  127. </div>
  128. </div>
  129. <a class="view-all-link" (click)="goToConsultationList()">查看全部</a>
  130. </div>
  131. <div class="crm-list">
  132. <div class="empty-state small">暂无待回访客户</div>
  133. </div>
  134. </div>
  135. </div>
  136. </section> -->
  137. <!-- 新增:待跟进尾款项目列表 -->
  138. <section class="pending-final-payment-section">
  139. <div class="section-header">
  140. <h3>待跟进尾款项目</h3>
  141. <div class="section-stats">
  142. <span class="stat-badge urgent">{{ pendingFinalPaymentProjects().length }}</span>
  143. <span class="stat-label">个项目待跟进</span>
  144. </div>
  145. </div>
  146. <div class="final-payment-list">
  147. @if (pendingFinalPaymentProjects().length === 0) {
  148. <div class="empty-state">
  149. <svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  150. <circle cx="12" cy="12" r="10"></circle>
  151. <path d="M12 6v6l4 2"></path>
  152. </svg>
  153. <p>暂无待跟进尾款项目</p>
  154. </div>
  155. }
  156. @for (project of pendingFinalPaymentProjects(); track project.id) {
  157. <div class="final-payment-item" [class.overdue]="project.status === '已逾期'" [class.warning]="project.status === '待创建'">
  158. <div class="project-info">
  159. <div class="project-header">
  160. <h4 class="project-name">{{ project.projectName }}</h4>
  161. <div class="payment-summary">
  162. <span class="payment-amount remaining" title="剩余未付">¥{{ project.finalPaymentAmount | number:'1.0-0' }}</span>
  163. <span class="payment-details">
  164. <small>订单总额: ¥{{ project.totalAmount | number:'1.0-0' }}</small>
  165. <small>已付: ¥{{ project.paidAmount | number:'1.0-0' }}</small>
  166. </span>
  167. </div>
  168. </div>
  169. <div class="customer-info">
  170. <div class="customer-details">
  171. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" class="icon-user">
  172. <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
  173. <circle cx="12" cy="7" r="4"></circle>
  174. </svg>
  175. <span class="customer-name">{{ project.customerName }}</span>
  176. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" class="icon-phone">
  177. <path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path>
  178. </svg>
  179. <span class="customer-phone">{{ project.customerPhone }}</span>
  180. </div>
  181. <div class="project-meta">
  182. <span class="due-date">
  183. <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  184. <circle cx="12" cy="12" r="10"></circle>
  185. <polyline points="12 6 12 12 16 14"></polyline>
  186. </svg>
  187. 应付时间:{{ project.dueDate | date:'yyyy-MM-dd' }}
  188. </span>
  189. <span class="status-badge"
  190. [ngClass]="{
  191. 'overdue': project.status === '已逾期',
  192. 'pending': project.status === '待付款',
  193. 'warning': project.status === '待创建'
  194. }">
  195. {{ project.status }}
  196. @if (project.overdueDay > 0) {
  197. <span class="overdue-days">(逾期{{ project.overdueDay }}天)</span>
  198. }
  199. </span>
  200. </div>
  201. </div>
  202. <!-- 进度条显示 -->
  203. <div class="payment-progress-bar">
  204. <div class="progress-info">
  205. <span class="progress-label">付款进度</span>
  206. <span class="progress-percent">{{ (project.paidAmount / project.totalAmount * 100) | number:'1.0-0' }}%</span>
  207. </div>
  208. <div class="progress-track">
  209. <div class="progress-fill" [style.width.%]="(project.paidAmount / project.totalAmount * 100)"></div>
  210. </div>
  211. </div>
  212. </div>
  213. <div class="payment-actions">
  214. <button
  215. class="btn-primary mini"
  216. (click)="followUpFinalPayment(project.projectId)"
  217. title="开始跟进客户尾款"
  218. >
  219. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  220. <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
  221. </svg>
  222. 开始跟进
  223. </button>
  224. <button
  225. class="btn-secondary mini"
  226. (click)="viewProjectDetail(project.projectId)"
  227. title="查看项目详情"
  228. >
  229. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  230. <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
  231. <circle cx="12" cy="12" r="3"></circle>
  232. </svg>
  233. 查看详情
  234. </button>
  235. </div>
  236. </div>
  237. }
  238. </div>
  239. </section>
  240. <!-- 🆕 待办任务双栏布局(待办问题 + 紧急事件) -->
  241. <section class="urgent-tasks-section">
  242. <div class="section-header">
  243. <h2>待办事项</h2>
  244. </div>
  245. <!-- 🆕 双栏容器 -->
  246. <div class="todo-dual-columns">
  247. <!-- ========== 左栏:紧急事件 ========== -->
  248. <div class="todo-column todo-column-urgent">
  249. <div class="column-header">
  250. <h3>
  251. <svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
  252. <path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>
  253. </svg>
  254. 紧急事件
  255. @if (urgentEventsList().length > 0) {
  256. <span class="task-count urgent">({{ urgentEventsList().length }})</span>
  257. }
  258. </h3>
  259. <span class="column-subtitle">自动计算的截止事件</span>
  260. </div>
  261. <!-- 🆕 标签筛选栏 -->
  262. <div class="tag-filter-bar">
  263. <button
  264. class="tag-button"
  265. [class.active]="urgentEventTagFilter() === 'all'"
  266. (click)="filterUrgentEventsByTag('all')"
  267. title="显示所有待办事项"
  268. >
  269. <span class="tag-icon">📋</span>
  270. <span class="tag-label">全部</span>
  271. <span class="tag-count">{{ urgentEventsList().length }}</span>
  272. </button>
  273. <button
  274. class="tag-button"
  275. [class.active]="urgentEventTagFilter() === 'customer'"
  276. (click)="filterUrgentEventsByTag('customer')"
  277. title="客户服务预警"
  278. >
  279. <span class="tag-icon">👥</span>
  280. <span class="tag-label">客户服务</span>
  281. <span class="tag-count">{{ getTagCount('customer') }}</span>
  282. </button>
  283. <!-- 工作阶段标签 -->
  284. <button
  285. class="tag-button"
  286. [class.active]="urgentEventTagFilter() === 'phase'"
  287. (click)="filterUrgentEventsByTag('phase')"
  288. title="制图阶段"
  289. >
  290. <span class="tag-icon">🔧</span>
  291. <span class="tag-label">制图阶段</span>
  292. <span class="tag-count">{{ getTagCount('phase') }}</span>
  293. </button>
  294. <!-- 小图截止标签 -->
  295. <button
  296. class="tag-button"
  297. [class.active]="urgentEventTagFilter() === 'review'"
  298. (click)="filterUrgentEventsByTag('review')"
  299. title="小图截止"
  300. >
  301. <span class="tag-icon">📐</span>
  302. <span class="tag-label">小图截止</span>
  303. <span class="tag-count">{{ getTagCount('review') }}</span>
  304. </button>
  305. <!-- 交付延期标签 -->
  306. <button
  307. class="tag-button"
  308. [class.active]="urgentEventTagFilter() === 'delivery'"
  309. (click)="filterUrgentEventsByTag('delivery')"
  310. title="交付延期"
  311. >
  312. <span class="tag-icon">📦</span>
  313. <span class="tag-label">交付延期</span>
  314. <span class="tag-count">{{ getTagCount('delivery') }}</span>
  315. </button>
  316. </div>
  317. <!-- 加载状态 -->
  318. @if (loadingUrgentEvents()) {
  319. <div class="loading-state">
  320. <svg class="spinner" viewBox="0 0 50 50">
  321. <circle cx="25" cy="25" r="20" fill="none" stroke-width="4"></circle>
  322. </svg>
  323. <p>计算紧急事件中...</p>
  324. </div>
  325. }
  326. <!-- 空状态 -->
  327. @if (!loadingUrgentEvents() && urgentEventsList().length === 0) {
  328. <div class="empty-state">
  329. <svg viewBox="0 0 24 24" width="64" height="64" fill="#d1d5db">
  330. <path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/>
  331. </svg>
  332. <p>暂无紧急事件</p>
  333. <p class="hint">所有项目时间节点正常 ✅</p>
  334. </div>
  335. }
  336. <!-- 紧急事件列表 -->
  337. @if (!loadingUrgentEvents() && filteredUrgentEvents().length > 0) {
  338. <div class="todo-list-compact urgent-list">
  339. @for (event of filteredUrgentEvents(); track event.id) {
  340. <div class="todo-item-compact urgent-item" [attr.data-urgency]="event.urgencyLevel">
  341. <!-- 左侧紧急程度色条 -->
  342. <div class="urgency-indicator" [attr.data-urgency]="event.urgencyLevel"></div>
  343. <!-- 事件内容 -->
  344. <div class="task-content">
  345. <!-- 标题行 -->
  346. <div class="task-header">
  347. <span class="task-title">{{ event.title }}</span>
  348. <div class="task-badges">
  349. <span class="badge badge-urgency" [attr.data-urgency]="event.urgencyLevel">
  350. @if (event.urgencyLevel === 'critical') { 🔴 紧急 }
  351. @else if (event.urgencyLevel === 'high') { 🟠 重要 }
  352. @else { 🟡 注意 }
  353. </span>
  354. <span class="badge badge-event-type">
  355. @if (event.eventType === 'review') { 对图 }
  356. @else if (event.eventType === 'delivery') { 交付 }
  357. @else if (event.eventType === 'phase_deadline') { {{ event.phaseName }} }
  358. @else if (event.category === 'customer') { 客户 }
  359. </span>
  360. <span class="badge badge-status overdue" *ngIf="event.statusType === 'overdue'">逾期</span>
  361. <span class="badge badge-status upcoming" *ngIf="event.statusType === 'dueSoon'">临近</span>
  362. <span class="badge badge-status stagnant" *ngIf="event.statusType === 'stagnant'">
  363. 停滞{{ event.stagnationDays || 7 }}天
  364. </span>
  365. <span class="badge badge-status customer" *ngIf="getEventCategory(event) === 'customer'">客户预警</span>
  366. </div>
  367. </div>
  368. <!-- 描述 -->
  369. <div class="task-description">
  370. {{ event.description }}
  371. </div>
  372. @if (event.followUpNeeded) {
  373. <div class="followup-tip">
  374. 客户反馈待跟进 · 请尽快处理
  375. </div>
  376. }
  377. <!-- 项目信息行 -->
  378. <div class="task-meta">
  379. <span class="project-info">
  380. <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
  381. <path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
  382. </svg>
  383. 项目: {{ event.projectName }}
  384. </span>
  385. @if (event.designerName) {
  386. <span class="designer-info">
  387. <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
  388. <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"/>
  389. </svg>
  390. 设计师: {{ event.designerName }}
  391. </span>
  392. }
  393. </div>
  394. <!-- 底部信息行 -->
  395. <div class="task-footer">
  396. <span class="deadline-info" [class.overdue]="event.overdueDays && event.overdueDays > 0">
  397. <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
  398. <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"/>
  399. </svg>
  400. 截止: {{ event.deadline | date:'MM-dd HH:mm' }}
  401. @if (event.overdueDays && event.overdueDays > 0) {
  402. <span class="overdue-label">(逾期{{ event.overdueDays }}天)</span>
  403. }
  404. @else if (event.overdueDays && event.overdueDays < 0) {
  405. <span class="upcoming-label">(还剩{{ -event.overdueDays }}天)</span>
  406. }
  407. @else {
  408. <span class="today-label">(今天)</span>
  409. }
  410. </span>
  411. @if (event.completionRate !== undefined) {
  412. <span class="completion-info">
  413. <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
  414. <path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/>
  415. </svg>
  416. 完成率: {{ event.completionRate }}%
  417. </span>
  418. }
  419. </div>
  420. </div>
  421. <!-- 右侧操作按钮 -->
  422. <div class="task-actions">
  423. <button
  424. class="btn-action btn-muted"
  425. *ngIf="event.allowConfirmOnTime"
  426. (click)="confirmEventOnTime(event)"
  427. title="确认可按时交付后隐藏该事件"
  428. >
  429. 可按时交付
  430. </button>
  431. <button
  432. class="btn-action btn-stagnant"
  433. *ngIf="event.statusType !== 'stagnant'"
  434. (click)="markEventAsStagnant(event)"
  435. title="标记为客户停滞期"
  436. >
  437. 标记停滞
  438. </button>
  439. <button
  440. class="btn-action btn-resolve"
  441. *ngIf="event.allowMarkHandled"
  442. (click)="resolveUrgentEvent(event)"
  443. title="事件已处理,不再提醒"
  444. >
  445. 事件已处理
  446. </button>
  447. <button
  448. class="btn-action btn-todo"
  449. *ngIf="event.allowCreateTodo"
  450. (click)="createTodoFromEvent(event)"
  451. title="将该事件生成代办任务"
  452. >
  453. 创建代办
  454. </button>
  455. <button
  456. class="btn-action btn-view"
  457. (click)="onUrgentEventViewProject(event.projectId)"
  458. title="查看项目">
  459. <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
  460. <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"/>
  461. </svg>
  462. 查看项目
  463. </button>
  464. </div>
  465. </div>
  466. }
  467. </div>
  468. }
  469. <!-- 过滤后没有结果的空状态 -->
  470. @if (!loadingUrgentEvents() && urgentEventsList().length > 0 && filteredUrgentEvents().length === 0) {
  471. <div class="empty-state filtered">
  472. <svg viewBox="0 0 24 24" width="48" height="48" fill="#d1d5db">
  473. <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"/>
  474. </svg>
  475. <p>该筛选条件下暂无事件</p>
  476. <p class="hint">尝试调整筛选条件</p>
  477. </div>
  478. }
  479. </div>
  480. <!-- ========== 左栏结束 ========== -->
  481. <!-- ========== 右栏:待办任务 ========== -->
  482. <div class="todo-column todo-column-issues">
  483. <div class="column-header">
  484. <h3>
  485. <svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
  486. <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"/>
  487. </svg>
  488. 待办任务
  489. @if (todoTasksFromIssues().length > 0) {
  490. <span class="task-count">({{ todoTasksFromIssues().length }})</span>
  491. }
  492. </h3>
  493. <span class="column-subtitle">来自项目问题板块</span>
  494. </div>
  495. <!-- 加载状态 -->
  496. @if (loadingTodoTasks()) {
  497. <div class="loading-state">
  498. <svg class="spinner" viewBox="0 0 50 50">
  499. <circle cx="25" cy="25" r="20" fill="none" stroke-width="4"></circle>
  500. </svg>
  501. <p>加载待办任务中...</p>
  502. </div>
  503. }
  504. <!-- 错误状态 -->
  505. @if (!loadingTodoTasks() && todoTaskError()) {
  506. <div class="error-state">
  507. <svg viewBox="0 0 24 24" width="48" height="48" fill="#ef4444">
  508. <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"/>
  509. </svg>
  510. <p>{{ todoTaskError() }}</p>
  511. <button class="btn-retry" (click)="refreshTodoTasks()">重试</button>
  512. </div>
  513. }
  514. <!-- 空状态 -->
  515. @if (!loadingTodoTasks() && !todoTaskError() && todoTasksFromIssues().length === 0) {
  516. <div class="empty-state">
  517. <svg viewBox="0 0 24 24" width="64" height="64" fill="#d1d5db">
  518. <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"/>
  519. </svg>
  520. <p>暂无待办任务</p>
  521. <p class="hint">所有项目问题都已处理完毕 🎉</p>
  522. </div>
  523. }
  524. <!-- 待办任务列表 -->
  525. @if (!loadingTodoTasks() && !todoTaskError() && todoTasksFromIssues().length > 0) {
  526. <div class="todo-list-compact">
  527. @for (task of todoTasksFromIssues(); track task.id) {
  528. <div class="todo-item-compact" [attr.data-priority]="task.priority">
  529. <!-- 左侧优先级色条 -->
  530. <div class="priority-indicator" [attr.data-priority]="task.priority"></div>
  531. <!-- 任务内容 -->
  532. <div class="task-content">
  533. <!-- 标题行 -->
  534. <div class="task-header">
  535. <span class="task-title">{{ task.title }}</span>
  536. <div class="task-badges">
  537. <span class="badge badge-priority" [attr.data-priority]="task.priority">
  538. {{ getPriorityConfig(task.priority).label }}
  539. </span>
  540. <span class="badge badge-type">{{ getIssueTypeLabel(task.type) }}</span>
  541. </div>
  542. </div>
  543. <!-- 项目信息行 -->
  544. <div class="task-meta">
  545. <span class="project-info">
  546. <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
  547. <path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
  548. </svg>
  549. 项目: {{ task.projectName }}
  550. @if (task.relatedSpace) {
  551. | {{ task.relatedSpace }}
  552. }
  553. @if (task.relatedStage) {
  554. | {{ task.relatedStage }}
  555. }
  556. </span>
  557. </div>
  558. <!-- 底部信息行 -->
  559. <div class="task-footer">
  560. <span class="time-info" [title]="formatExactTime(task.createdAt)">
  561. <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
  562. <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"/>
  563. </svg>
  564. 创建于 {{ formatRelativeTime(task.createdAt) }}
  565. </span>
  566. <span class="assignee-info">
  567. <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
  568. <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"/>
  569. </svg>
  570. 指派给: {{ task.assigneeName }}
  571. </span>
  572. </div>
  573. </div>
  574. <!-- 右侧操作按钮 -->
  575. <div class="task-actions">
  576. <button
  577. class="btn-action btn-view"
  578. (click)="navigateToIssue(task)"
  579. title="查看详情">
  580. <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
  581. <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"/>
  582. </svg>
  583. 查看详情
  584. </button>
  585. <button
  586. class="btn-action btn-mark-read"
  587. (click)="onTodoTaskMarkAsRead(task)"
  588. title="标记已读">
  589. <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
  590. <path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/>
  591. </svg>
  592. 标记已读
  593. </button>
  594. </div>
  595. </div>
  596. }
  597. </div>
  598. }
  599. </div>
  600. <!-- ========== 右栏结束 ========== -->
  601. </div>
  602. <!-- ========== 双栏容器结束 ========== -->
  603. </section>
  604. <!-- iOS风格的添加紧急事项面板 -->
  605. @if (isTaskFormVisible()) {
  606. <div class="ios-modal-overlay" (click)="hideTaskForm()">
  607. <div class="ios-panel" (click)="$event.stopPropagation()">
  608. <div class="ios-panel-header">
  609. <h3>添加紧急事项</h3>
  610. <button class="ios-close-button" (click)="hideTaskForm()">
  611. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  612. <line x1="18" y1="6" x2="6" y2="18"></line>
  613. <line x1="6" y1="6" x2="18" y2="18"></line>
  614. </svg>
  615. </button>
  616. </div>
  617. <div class="ios-panel-content">
  618. <form (ngSubmit)="handleAddTaskSubmit()">
  619. <div class="form-group">
  620. <label for="taskTitle">任务标题 *</label>
  621. <input
  622. type="text"
  623. id="taskTitle"
  624. [(ngModel)]="newTask.title"
  625. [ngModelOptions]="{standalone: true}"
  626. placeholder="请输入任务标题"
  627. required
  628. class="ios-input"
  629. >
  630. </div>
  631. <div class="form-group">
  632. <label for="projectSelect">选择项目 *</label>
  633. <select
  634. id="projectSelect"
  635. [(ngModel)]="newTask.projectId"
  636. [ngModelOptions]="{standalone: true}"
  637. (change)="onProjectChange($any($event.target).value)"
  638. required
  639. class="ios-select"
  640. >
  641. <option value="">-- 请选择项目 --</option>
  642. @for (project of projectList(); track project.id) {
  643. <option [value]="project.id">{{ project.title }}</option>
  644. }
  645. </select>
  646. </div>
  647. <div class="form-group">
  648. <label for="spaceSelect">选择空间(可选)</label>
  649. <select
  650. id="spaceSelect"
  651. [(ngModel)]="newTask.spaceId"
  652. [ngModelOptions]="{standalone: true}"
  653. class="ios-select"
  654. [disabled]="!newTask.projectId"
  655. >
  656. <option value="">-- 请选择空间 --</option>
  657. @for (space of spaceList(); track space.id) {
  658. <option [value]="space.id">{{ space.title }}</option>
  659. }
  660. </select>
  661. </div>
  662. <div class="form-group">
  663. <label for="projectStage">项目阶段 *</label>
  664. <select
  665. id="projectStage"
  666. [(ngModel)]="newTask.stage"
  667. [ngModelOptions]="{standalone: true}"
  668. required
  669. class="ios-select"
  670. >
  671. <option value="订单分配">订单分配</option>
  672. <option value="慎设需求">慎设需求</option>
  673. <option value="交付执行">交付执行</option>
  674. <option value="售后">售后</option>
  675. </select>
  676. </div>
  677. <div class="form-group">
  678. <label for="region">区域/位置(可选)</label>
  679. <input
  680. type="text"
  681. id="region"
  682. [(ngModel)]="newTask.region"
  683. [ngModelOptions]="{standalone: true}"
  684. placeholder="例如:客厅、主卧、厨房"
  685. class="ios-input"
  686. >
  687. </div>
  688. <div class="form-group">
  689. <label for="taskDeadline">截止时间 *</label>
  690. <!-- 显示当前选择的截止时间 -->
  691. <div class="deadline-display ios-input" (click)="deadlineDropdownVisible = !deadlineDropdownVisible">
  692. {{ getDisplayDeadline() || '请选择截止时间' }}
  693. <span class="dropdown-arrow" [class.rotate]="deadlineDropdownVisible">▼</span>
  694. </div>
  695. <!-- 预设时长下拉选择框 -->
  696. <div class="deadline-dropdown" [class.visible]="deadlineDropdownVisible">
  697. @for (preset of timePresets; track preset.hours) {
  698. <div class="dropdown-option"
  699. [class.selected]="selectedPreset === preset.hours.toString()"
  700. (click)="handlePresetSelection(preset.hours.toString())">
  701. {{ preset.label }}
  702. </div>
  703. }
  704. <!-- 当天24:00前选项 -->
  705. <div class="dropdown-option"
  706. [class.selected]="selectedPreset === 'today'"
  707. (click)="handlePresetSelection('today')">
  708. 今日24:00前
  709. </div>
  710. <!-- 自定义时间选项 -->
  711. <div class="dropdown-divider"></div>
  712. <div class="dropdown-option custom-option" (click)="handlePresetSelection('custom')">
  713. 自定义时间
  714. </div>
  715. </div>
  716. <!-- 错误提示信息 -->
  717. @if (deadlineError) {
  718. <div class="error-message">{{ deadlineError }}</div>
  719. }
  720. </div>
  721. <!-- 自定义时间选择弹窗 -->
  722. @if (isCustomTimeVisible) {
  723. <div class="custom-time-modal">
  724. <div class="modal-backdrop"></div>
  725. <div class="modal-content">
  726. <div class="modal-header">
  727. <h3>选择自定义时间</h3>
  728. <button class="close-button" (click)="closeCustomTimePicker()">×</button>
  729. </div>
  730. <div class="modal-body">
  731. <!-- 日期选择 -->
  732. <div class="date-picker">
  733. <label>日期</label>
  734. <input
  735. type="date"
  736. [(ngModel)]="customDate"
  737. min="{{ todayDate }}"
  738. max="{{ sevenDaysLaterDate }}"
  739. class="ios-input"
  740. />
  741. </div>
  742. <!-- 时间选择 -->
  743. <div class="time-picker">
  744. <label>时间</label>
  745. <input
  746. type="time"
  747. [(ngModel)]="customTime"
  748. class="ios-input"
  749. />
  750. </div>
  751. <!-- 错误提示信息 -->
  752. @if (deadlineError) {
  753. <div class="error-message">{{ deadlineError }}</div>
  754. }
  755. </div>
  756. <div class="modal-footer">
  757. <button class="cancel-button" (click)="closeCustomTimePicker()">取消</button>
  758. <button class="confirm-button" (click)="handleCustomTimeSelection()">确定</button>
  759. </div>
  760. </div>
  761. </div>
  762. }
  763. <div class="form-group">
  764. <label for="assigneeSelect">指派给(可选)</label>
  765. <select
  766. id="assigneeSelect"
  767. [(ngModel)]="newTask.assigneeId"
  768. [ngModelOptions]="{standalone: true}"
  769. class="ios-select"
  770. >
  771. <option value="">-- 暂不指派 --</option>
  772. @for (member of teamMembers(); track member.id) {
  773. <option [value]="member.id">{{ member.name }}{{ member.roleName ? ' (' + member.roleName + ')' : '' }}</option>
  774. }
  775. </select>
  776. </div>
  777. <div class="form-group">
  778. <label for="taskPriority">优先级 *</label>
  779. <select
  780. id="taskPriority"
  781. [(ngModel)]="newTask.priority"
  782. [ngModelOptions]="{standalone: true}"
  783. class="ios-select"
  784. >
  785. <option value="high">🔴 高优先级(紧急)</option>
  786. <option value="medium">🟡 中优先级(普通)</option>
  787. <option value="low">🟢 低优先级</option>
  788. </select>
  789. </div>
  790. <div class="form-group">
  791. <label for="taskDescription">详细描述(可选)</label>
  792. <textarea
  793. id="taskDescription"
  794. [(ngModel)]="newTask.description"
  795. [ngModelOptions]="{standalone: true}"
  796. placeholder="请详细描述需要紧急处理的问题..."
  797. rows="4"
  798. class="ios-textarea"
  799. ></textarea>
  800. </div>
  801. </form>
  802. </div>
  803. <div class="ios-panel-footer">
  804. <button type="button" class="ios-cancel-button" (click)="hideTaskForm()">取消</button>
  805. <button type="button" class="ios-submit-button" (click)="handleAddTaskSubmit()" [disabled]="isSubmitDisabled">确定</button>
  806. </div>
  807. </div>
  808. </div>
  809. }
  810. <!-- 待办任务流(复用组长端设计) - 已隐藏 -->
  811. <!-- <section class="project-updates-section todo-section-customer-service">
  812. ... 已隐藏的待办任务流代码 ...
  813. </section> -->
  814. <!-- 回到顶部按钮 -->
  815. <button class="back-to-top" (click)="scrollToTop()" [class.visible]="showBackToTop()">
  816. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  817. <polyline points="18 15 12 9 6 15"></polyline>
  818. </svg>
  819. </button>