dashboard.html 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722
  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. <div class="content-grid">
  242. <!-- 紧急事件列表(⭐ 使用可复用组件) -->
  243. <section class="urgent-tasks-section">
  244. <div class="section-header">
  245. <h3>紧急事件</h3>
  246. <div style="display: flex; gap: 12px; align-items: center;">
  247. <button
  248. class="btn-primary"
  249. (click)="showTaskForm()"
  250. style="font-size: 14px; padding: 6px 16px;"
  251. >
  252. 添加紧急事项
  253. </button>
  254. <a href="/customer-service/project-list" class="view-all-link">查看全部</a>
  255. </div>
  256. </div>
  257. <div class="tasks-list">
  258. @if (loadingUrgentEvents()) {
  259. <div class="loading-state">
  260. <svg class="spinner" viewBox="0 0 50 50">
  261. <circle cx="25" cy="25" r="20" fill="none" stroke-width="4"></circle>
  262. </svg>
  263. <p>计算紧急事件中...</p>
  264. </div>
  265. }
  266. @if (!loadingUrgentEvents() && urgentEventsList().length === 0) {
  267. <div class="empty-state">
  268. <svg viewBox="0 0 24 24" width="64" height="64" fill="#d1d5db">
  269. <path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/>
  270. </svg>
  271. <p>暂无紧急事件</p>
  272. <p class="hint">所有项目时间节点正常 ✅</p>
  273. </div>
  274. }
  275. @if (!loadingUrgentEvents() && urgentEventsList().length > 0) {
  276. <div class="todo-list-compact urgent-list">
  277. @for (event of urgentEventsList(); track event.id) {
  278. <div class="todo-item-compact urgent-item" [attr.data-urgency]="event.urgencyLevel">
  279. <div class="urgency-indicator" [attr.data-urgency]="event.urgencyLevel"></div>
  280. <div class="task-content">
  281. <div class="task-header">
  282. <span class="task-title">{{ event.title }}</span>
  283. <div class="task-badges">
  284. <span class="badge badge-urgency" [attr.data-urgency]="event.urgencyLevel">
  285. @if (event.urgencyLevel === 'critical') { 🔴 紧急 }
  286. @else if (event.urgencyLevel === 'high') { 🟠 重要 }
  287. @else { 🟡 注意 }
  288. </span>
  289. <span class="badge badge-event-type">
  290. @if (event.eventType === 'review') { 对图 }
  291. @else if (event.eventType === 'delivery') { 交付 }
  292. @else if (event.eventType === 'phase_deadline') { {{ event.phaseName }} }
  293. </span>
  294. </div>
  295. </div>
  296. <div class="task-description">{{ event.description }}</div>
  297. <div class="task-meta">
  298. <span class="project-info">
  299. <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
  300. <path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
  301. </svg>
  302. 项目: {{ event.projectName }}
  303. </span>
  304. @if (event.designerName) {
  305. <span class="designer-info">
  306. <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
  307. <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"/>
  308. </svg>
  309. 设计师: {{ event.designerName }}
  310. </span>
  311. }
  312. </div>
  313. <div class="task-footer">
  314. <span class="deadline-info" [class.overdue]="event.overdueDays && event.overdueDays > 0">
  315. <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
  316. <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"/>
  317. </svg>
  318. 截止: {{ event.deadline | date:'MM-dd HH:mm' }}
  319. @if (event.overdueDays && event.overdueDays > 0) { <span class="overdue-label">(逾期{{ event.overdueDays }}天)</span> }
  320. @else if (event.overdueDays && event.overdueDays < 0) { <span class="upcoming-label">(还剩{{ -event.overdueDays }}天)</span> }
  321. @else { <span class="today-label">(今天)</span> }
  322. </span>
  323. @if (event.completionRate !== undefined) {
  324. <span class="completion-info">
  325. <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
  326. <path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/>
  327. </svg>
  328. 完成率: {{ event.completionRate }}%
  329. </span>
  330. }
  331. </div>
  332. </div>
  333. <div class="task-actions">
  334. <button class="btn-action btn-view" (click)="onUrgentEventViewProject(event.projectId)">查看项目</button>
  335. </div>
  336. </div>
  337. }
  338. </div>
  339. }
  340. </div>
  341. </section>
  342. <!-- iOS风格的添加紧急事项面板 -->
  343. @if (isTaskFormVisible()) {
  344. <div class="ios-modal-overlay" (click)="hideTaskForm()">
  345. <div class="ios-panel" (click)="$event.stopPropagation()">
  346. <div class="ios-panel-header">
  347. <h3>添加紧急事项</h3>
  348. <button class="ios-close-button" (click)="hideTaskForm()">
  349. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  350. <line x1="18" y1="6" x2="6" y2="18"></line>
  351. <line x1="6" y1="6" x2="18" y2="18"></line>
  352. </svg>
  353. </button>
  354. </div>
  355. <div class="ios-panel-content">
  356. <form (ngSubmit)="handleAddTaskSubmit()">
  357. <div class="form-group">
  358. <label for="taskTitle">任务标题 *</label>
  359. <input
  360. type="text"
  361. id="taskTitle"
  362. [(ngModel)]="newTask.title"
  363. [ngModelOptions]="{standalone: true}"
  364. placeholder="请输入任务标题"
  365. required
  366. class="ios-input"
  367. >
  368. </div>
  369. <div class="form-group">
  370. <label for="projectSelect">选择项目 *</label>
  371. <select
  372. id="projectSelect"
  373. [(ngModel)]="newTask.projectId"
  374. [ngModelOptions]="{standalone: true}"
  375. (change)="onProjectChange($any($event.target).value)"
  376. required
  377. class="ios-select"
  378. >
  379. <option value="">-- 请选择项目 --</option>
  380. @for (project of projectList(); track project.id) {
  381. <option [value]="project.id">{{ project.title }}</option>
  382. }
  383. </select>
  384. </div>
  385. <div class="form-group">
  386. <label for="spaceSelect">选择空间(可选)</label>
  387. <select
  388. id="spaceSelect"
  389. [(ngModel)]="newTask.spaceId"
  390. [ngModelOptions]="{standalone: true}"
  391. class="ios-select"
  392. [disabled]="!newTask.projectId"
  393. >
  394. <option value="">-- 请选择空间 --</option>
  395. @for (space of spaceList(); track space.id) {
  396. <option [value]="space.id">{{ space.title }}</option>
  397. }
  398. </select>
  399. </div>
  400. <div class="form-group">
  401. <label for="projectStage">项目阶段 *</label>
  402. <select
  403. id="projectStage"
  404. [(ngModel)]="newTask.stage"
  405. [ngModelOptions]="{standalone: true}"
  406. required
  407. class="ios-select"
  408. >
  409. <option value="订单分配">订单分配</option>
  410. <option value="慎设需求">慎设需求</option>
  411. <option value="交付执行">交付执行</option>
  412. <option value="售后">售后</option>
  413. </select>
  414. </div>
  415. <div class="form-group">
  416. <label for="region">区域/位置(可选)</label>
  417. <input
  418. type="text"
  419. id="region"
  420. [(ngModel)]="newTask.region"
  421. [ngModelOptions]="{standalone: true}"
  422. placeholder="例如:客厅、主卧、厨房"
  423. class="ios-input"
  424. >
  425. </div>
  426. <div class="form-group">
  427. <label for="taskDeadline">截止时间 *</label>
  428. <!-- 显示当前选择的截止时间 -->
  429. <div class="deadline-display ios-input" (click)="deadlineDropdownVisible = !deadlineDropdownVisible">
  430. {{ getDisplayDeadline() || '请选择截止时间' }}
  431. <span class="dropdown-arrow" [class.rotate]="deadlineDropdownVisible">▼</span>
  432. </div>
  433. <!-- 预设时长下拉选择框 -->
  434. <div class="deadline-dropdown" [class.visible]="deadlineDropdownVisible">
  435. @for (preset of timePresets; track preset.hours) {
  436. <div class="dropdown-option"
  437. [class.selected]="selectedPreset === preset.hours.toString()"
  438. (click)="handlePresetSelection(preset.hours.toString())">
  439. {{ preset.label }}
  440. </div>
  441. }
  442. <!-- 当天24:00前选项 -->
  443. <div class="dropdown-option"
  444. [class.selected]="selectedPreset === 'today'"
  445. (click)="handlePresetSelection('today')">
  446. 今日24:00前
  447. </div>
  448. <!-- 自定义时间选项 -->
  449. <div class="dropdown-divider"></div>
  450. <div class="dropdown-option custom-option" (click)="handlePresetSelection('custom')">
  451. 自定义时间
  452. </div>
  453. </div>
  454. <!-- 错误提示信息 -->
  455. @if (deadlineError) {
  456. <div class="error-message">{{ deadlineError }}</div>
  457. }
  458. </div>
  459. <!-- 自定义时间选择弹窗 -->
  460. @if (isCustomTimeVisible) {
  461. <div class="custom-time-modal">
  462. <div class="modal-backdrop"></div>
  463. <div class="modal-content">
  464. <div class="modal-header">
  465. <h3>选择自定义时间</h3>
  466. <button class="close-button" (click)="closeCustomTimePicker()">×</button>
  467. </div>
  468. <div class="modal-body">
  469. <!-- 日期选择 -->
  470. <div class="date-picker">
  471. <label>日期</label>
  472. <input
  473. type="date"
  474. [(ngModel)]="customDate"
  475. min="{{ todayDate }}"
  476. max="{{ sevenDaysLaterDate }}"
  477. class="ios-input"
  478. />
  479. </div>
  480. <!-- 时间选择 -->
  481. <div class="time-picker">
  482. <label>时间</label>
  483. <input
  484. type="time"
  485. [(ngModel)]="customTime"
  486. class="ios-input"
  487. />
  488. </div>
  489. <!-- 错误提示信息 -->
  490. @if (deadlineError) {
  491. <div class="error-message">{{ deadlineError }}</div>
  492. }
  493. </div>
  494. <div class="modal-footer">
  495. <button class="cancel-button" (click)="closeCustomTimePicker()">取消</button>
  496. <button class="confirm-button" (click)="handleCustomTimeSelection()">确定</button>
  497. </div>
  498. </div>
  499. </div>
  500. }
  501. <div class="form-group">
  502. <label for="assigneeSelect">指派给(可选)</label>
  503. <select
  504. id="assigneeSelect"
  505. [(ngModel)]="newTask.assigneeId"
  506. [ngModelOptions]="{standalone: true}"
  507. class="ios-select"
  508. >
  509. <option value="">-- 暂不指派 --</option>
  510. @for (member of teamMembers(); track member.id) {
  511. <option [value]="member.id">{{ member.name }}{{ member.roleName ? ' (' + member.roleName + ')' : '' }}</option>
  512. }
  513. </select>
  514. </div>
  515. <div class="form-group">
  516. <label for="taskPriority">优先级 *</label>
  517. <select
  518. id="taskPriority"
  519. [(ngModel)]="newTask.priority"
  520. [ngModelOptions]="{standalone: true}"
  521. class="ios-select"
  522. >
  523. <option value="high">🔴 高优先级(紧急)</option>
  524. <option value="medium">🟡 中优先级(普通)</option>
  525. <option value="low">🟢 低优先级</option>
  526. </select>
  527. </div>
  528. <div class="form-group">
  529. <label for="taskDescription">详细描述(可选)</label>
  530. <textarea
  531. id="taskDescription"
  532. [(ngModel)]="newTask.description"
  533. [ngModelOptions]="{standalone: true}"
  534. placeholder="请详细描述需要紧急处理的问题..."
  535. rows="4"
  536. class="ios-textarea"
  537. ></textarea>
  538. </div>
  539. </form>
  540. </div>
  541. <div class="ios-panel-footer">
  542. <button type="button" class="ios-cancel-button" (click)="hideTaskForm()">取消</button>
  543. <button type="button" class="ios-submit-button" (click)="handleAddTaskSubmit()" [disabled]="isSubmitDisabled">确定</button>
  544. </div>
  545. </div>
  546. </div>
  547. }
  548. <!-- 待办任务流(复用组长端设计) -->
  549. <section class="project-updates-section todo-section-customer-service">
  550. <div class="section-header">
  551. <h2>
  552. 待办任务
  553. @if (todoTasksFromIssues().length > 0) {
  554. <span class="task-count">({{ todoTasksFromIssues().length }})</span>
  555. }
  556. </h2>
  557. <button
  558. class="btn-refresh"
  559. (click)="onRefreshTodoTasks()"
  560. [disabled]="loadingTodoTasks()"
  561. title="刷新待办任务">
  562. <svg viewBox="0 0 24 24" width="16" height="16" [class.rotating]="loadingTodoTasks()">
  563. <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"/>
  564. </svg>
  565. </button>
  566. </div>
  567. <!-- 加载状态 -->
  568. @if (loadingTodoTasks()) {
  569. <div class="loading-state">
  570. <svg class="spinner" viewBox="0 0 50 50">
  571. <circle cx="25" cy="25" r="20" fill="none" stroke-width="4"></circle>
  572. </svg>
  573. <p>加载待办任务中...</p>
  574. </div>
  575. }
  576. <!-- 错误状态 -->
  577. @if (!loadingTodoTasks() && todoTaskError()) {
  578. <div class="error-state">
  579. <svg viewBox="0 0 24 24" width="48" height="48" fill="#ef4444">
  580. <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"/>
  581. </svg>
  582. <p>{{ todoTaskError() }}</p>
  583. <button class="btn-retry" (click)="onRefreshTodoTasks()">重试</button>
  584. </div>
  585. }
  586. <!-- 空状态 -->
  587. @if (!loadingTodoTasks() && !todoTaskError() && todoTasksFromIssues().length === 0) {
  588. <div class="empty-state">
  589. <svg viewBox="0 0 24 24" width="64" height="64" fill="#d1d5db">
  590. <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"/>
  591. </svg>
  592. <p>暂无待办任务</p>
  593. <p class="hint">所有项目问题都已处理完毕 🎉</p>
  594. </div>
  595. }
  596. <!-- 待办任务列表 -->
  597. @if (!loadingTodoTasks() && !todoTaskError() && todoTasksFromIssues().length > 0) {
  598. <div class="todo-list-compact">
  599. @for (task of todoTasksFromIssues(); track task.id) {
  600. <div class="todo-item-compact" [attr.data-priority]="task.priority">
  601. <!-- 左侧优先级色条 -->
  602. <div class="priority-indicator" [attr.data-priority]="task.priority"></div>
  603. <!-- 任务内容 -->
  604. <div class="task-content">
  605. <!-- 标题行 -->
  606. <div class="task-header">
  607. <span class="task-title">{{ task.title }}</span>
  608. <div class="task-badges">
  609. <span class="badge badge-priority" [attr.data-priority]="task.priority">
  610. {{ getPriorityConfig(task.priority).label }}
  611. </span>
  612. <span class="badge badge-type">{{ getIssueTypeLabel(task.type) }}</span>
  613. </div>
  614. </div>
  615. <!-- 项目信息行 -->
  616. <div class="task-meta">
  617. <span class="project-info">
  618. <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
  619. <path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
  620. </svg>
  621. 项目: {{ task.projectName }}
  622. @if (task.relatedSpace) {
  623. | {{ task.relatedSpace }}
  624. }
  625. @if (task.relatedStage) {
  626. | {{ task.relatedStage }}
  627. }
  628. </span>
  629. </div>
  630. <!-- 底部信息行 -->
  631. <div class="task-footer">
  632. <span class="time-info" [title]="formatExactTime(task.createdAt)">
  633. <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
  634. <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"/>
  635. </svg>
  636. 创建于 {{ formatRelativeTime(task.createdAt) }}
  637. </span>
  638. <span class="assignee-info">
  639. <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
  640. <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"/>
  641. </svg>
  642. 指派给: {{ task.assigneeName }}
  643. </span>
  644. </div>
  645. </div>
  646. <!-- 右侧操作按钮(⭐ 使用新的事件处理方法) -->
  647. <div class="task-actions">
  648. <button
  649. class="btn-action btn-view"
  650. (click)="onTodoTaskViewDetails(task)"
  651. title="查看详情">
  652. <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
  653. <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"/>
  654. </svg>
  655. 查看详情
  656. </button>
  657. <button
  658. class="btn-action btn-mark-read"
  659. (click)="onTodoTaskMarkAsRead(task)"
  660. title="标记已读">
  661. <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
  662. <path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/>
  663. </svg>
  664. 标记已读
  665. </button>
  666. </div>
  667. </div>
  668. }
  669. </div>
  670. }
  671. </section>
  672. <!-- 回到顶部按钮 -->
  673. <button class="back-to-top" (click)="scrollToTop()" [class.visible]="showBackToTop()">
  674. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  675. <polyline points="18 15 12 9 6 15"></polyline>
  676. </svg>
  677. </button>