dashboard.html 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  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 === '已逾期'">
  158. <div class="project-info">
  159. <div class="project-header">
  160. <h4 class="project-name">{{ project.projectName }}</h4>
  161. <span class="payment-amount highlight">¥{{ project.finalPaymentAmount | number:'1.0-0' }}</span>
  162. </div>
  163. <div class="customer-info">
  164. <div class="customer-details">
  165. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" class="icon-user">
  166. <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
  167. <circle cx="12" cy="7" r="4"></circle>
  168. </svg>
  169. <span class="customer-name">{{ project.customerName }}</span>
  170. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" class="icon-phone">
  171. <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>
  172. </svg>
  173. <span class="customer-phone">{{ project.customerPhone }}</span>
  174. </div>
  175. <div class="project-meta">
  176. <span class="due-date">
  177. <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  178. <circle cx="12" cy="12" r="10"></circle>
  179. <polyline points="12 6 12 12 16 14"></polyline>
  180. </svg>
  181. 应付时间:{{ project.dueDate | date:'yyyy-MM-dd' }}
  182. </span>
  183. <span class="status-badge" [ngClass]="{'overdue': project.status === '已逾期', 'pending': project.status === '待付款'}">
  184. {{ project.status }}
  185. @if (project.overdueDay > 0) {
  186. <span class="overdue-days">(逾期{{ project.overdueDay }}天)</span>
  187. }
  188. </span>
  189. </div>
  190. </div>
  191. </div>
  192. <div class="payment-actions">
  193. <button
  194. class="btn-primary mini"
  195. (click)="followUpFinalPayment(project.projectId)"
  196. title="开始跟进客户尾款"
  197. >
  198. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  199. <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>
  200. </svg>
  201. 开始跟进
  202. </button>
  203. <button
  204. class="btn-secondary mini"
  205. (click)="viewProjectDetail(project.projectId)"
  206. title="查看项目详情"
  207. >
  208. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  209. <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
  210. <circle cx="12" cy="12" r="3"></circle>
  211. </svg>
  212. 查看详情
  213. </button>
  214. </div>
  215. </div>
  216. }
  217. </div>
  218. </section>
  219. <!-- 紧急待办和项目动态流 -->
  220. <div class="content-grid">
  221. <!-- 紧急待办列表 -->
  222. <section class="urgent-tasks-section">
  223. <div class="section-header">
  224. <h3>紧急待办</h3>
  225. <div style="display: flex; gap: 12px; align-items: center;">
  226. <button
  227. class="btn-primary"
  228. (click)="showTaskForm()"
  229. style="font-size: 14px; padding: 6px 16px;"
  230. >
  231. 添加紧急事项
  232. </button>
  233. <a href="/customer-service/project-list" class="view-all-link">查看全部</a>
  234. </div>
  235. </div>
  236. <div class="tasks-list">
  237. @if (urgentTasks().length === 0) {
  238. <div class="empty-state">
  239. <svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  240. <circle cx="12" cy="12" r="10"></circle>
  241. <polyline points="12 6 12 12 16 14"></polyline>
  242. </svg>
  243. <p>暂无紧急待办事项</p>
  244. </div>
  245. }
  246. @for (task of urgentTasks(); track task.id) {
  247. <div class="task-item-enhanced" [class.completed]="task.isCompleted" [class.overdue]="task.isOverdue">
  248. <div class="task-priority-indicator" [class.high]="task.priority === 'high'" [class.medium]="task.priority === 'medium'" [class.low]="task.priority === 'low'"></div>
  249. <div class="task-main-content">
  250. <div class="task-header-row">
  251. <div class="task-checkbox">
  252. <input type="checkbox" [checked]="task.isCompleted" (change)="markTaskAsCompleted(task.id)">
  253. </div>
  254. <div class="task-info">
  255. <h4 class="task-title">{{ task.title || '未命名任务' }}</h4>
  256. <div class="task-tags">
  257. <span class="tag tag-project">📋 {{ task.projectName || '未知项目' }}</span>
  258. <span class="tag tag-stage">🔄 {{ task.stage }}</span>
  259. @if (task.assignee && task.assignee !== '未分配') {
  260. <span class="tag tag-assignee">👤 {{ task.assignee }}</span>
  261. }
  262. </div>
  263. </div>
  264. </div>
  265. @if (task.description) {
  266. <div class="task-description">
  267. <p>{{ task.description }}</p>
  268. </div>
  269. }
  270. <div class="task-meta-row">
  271. <div class="task-meta-info">
  272. <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  273. <circle cx="12" cy="12" r="10"></circle>
  274. <polyline points="12 6 12 12 16 14"></polyline>
  275. </svg>
  276. <span class="task-time" [class.overdue]="task.isOverdue">
  277. {{ formatDate(task.deadline) }}
  278. @if (task.isOverdue) {
  279. <span class="overdue-badge">已逾期</span>
  280. }
  281. </span>
  282. </div>
  283. <div class="task-priority-badge" [class.high]="task.priority === 'high'" [class.medium]="task.priority === 'medium'" [class.low]="task.priority === 'low'">
  284. {{ task.priority === 'high' ? '高优先级' : task.priority === 'medium' ? '中优先级' : '低优先级' }}
  285. </div>
  286. </div>
  287. <!-- 任务处理状态(使用本地变量 s 消除可选链告警) -->
  288. <ng-container *ngIf="taskProcessingState()[task.id] as s">
  289. <ng-container *ngIf="s.inProgress === true">
  290. <div class="task-progress-container">
  291. <div class="task-progress-bar" [style.width]="s.progress + '%'">
  292. <span class="task-progress-text">处理中 {{ s.progress }}%</span>
  293. </div>
  294. </div>
  295. </ng-container>
  296. </ng-container>
  297. </div>
  298. <div class="task-actions-row">
  299. <ng-container *ngIf="taskProcessingState()[task.id] as s; else noTaskActionState">
  300. <button
  301. class="btn-action btn-process"
  302. [class.processing]="s.inProgress === true"
  303. (click)="handleAssignment(task.id)"
  304. [disabled]="(s.inProgress === true) || task.isCompleted"
  305. >
  306. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  307. <polyline points="9 11 12 14 22 4"></polyline>
  308. <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
  309. </svg>
  310. {{ s.inProgress === true ? '处理中' : '处理' }}
  311. </button>
  312. </ng-container>
  313. <ng-template #noTaskActionState>
  314. <button class="btn-action btn-process" (click)="handleAssignment(task.id)" [disabled]="task.isCompleted">
  315. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  316. <polyline points="9 11 12 14 22 4"></polyline>
  317. <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
  318. </svg>
  319. 处理
  320. </button>
  321. </ng-template>
  322. <button class="btn-action btn-delete" (click)="deleteTask(task.id)" title="删除任务">
  323. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  324. <polyline points="3 6 5 6 21 6"></polyline>
  325. <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
  326. </svg>
  327. </button>
  328. </div>
  329. </div>
  330. }
  331. </div>
  332. </section>
  333. <!-- iOS风格的添加紧急事项面板 -->
  334. @if (isTaskFormVisible()) {
  335. <div class="ios-modal-overlay" (click)="hideTaskForm()">
  336. <div class="ios-panel" (click)="$event.stopPropagation()">
  337. <div class="ios-panel-header">
  338. <h3>添加紧急事项</h3>
  339. <button class="ios-close-button" (click)="hideTaskForm()">
  340. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  341. <line x1="18" y1="6" x2="6" y2="18"></line>
  342. <line x1="6" y1="6" x2="18" y2="18"></line>
  343. </svg>
  344. </button>
  345. </div>
  346. <div class="ios-panel-content">
  347. <form (ngSubmit)="handleAddTaskSubmit()">
  348. <div class="form-group">
  349. <label for="taskTitle">任务标题 *</label>
  350. <input
  351. type="text"
  352. id="taskTitle"
  353. [(ngModel)]="newTask.title"
  354. [ngModelOptions]="{standalone: true}"
  355. placeholder="请输入任务标题"
  356. required
  357. class="ios-input"
  358. >
  359. </div>
  360. <div class="form-group">
  361. <label for="projectSelect">选择项目 *</label>
  362. <select
  363. id="projectSelect"
  364. [(ngModel)]="newTask.projectId"
  365. [ngModelOptions]="{standalone: true}"
  366. (change)="onProjectChange($any($event.target).value)"
  367. required
  368. class="ios-select"
  369. >
  370. <option value="">-- 请选择项目 --</option>
  371. @for (project of projectList(); track project.id) {
  372. <option [value]="project.id">{{ project.title }}</option>
  373. }
  374. </select>
  375. </div>
  376. <div class="form-group">
  377. <label for="spaceSelect">选择空间(可选)</label>
  378. <select
  379. id="spaceSelect"
  380. [(ngModel)]="newTask.spaceId"
  381. [ngModelOptions]="{standalone: true}"
  382. class="ios-select"
  383. [disabled]="!newTask.projectId"
  384. >
  385. <option value="">-- 请选择空间 --</option>
  386. @for (space of spaceList(); track space.id) {
  387. <option [value]="space.id">{{ space.title }}</option>
  388. }
  389. </select>
  390. </div>
  391. <div class="form-group">
  392. <label for="projectStage">项目阶段 *</label>
  393. <select
  394. id="projectStage"
  395. [(ngModel)]="newTask.stage"
  396. [ngModelOptions]="{standalone: true}"
  397. required
  398. class="ios-select"
  399. >
  400. <option value="订单分配">订单分配</option>
  401. <option value="慎设需求">慎设需求</option>
  402. <option value="交付执行">交付执行</option>
  403. <option value="售后">售后</option>
  404. </select>
  405. </div>
  406. <div class="form-group">
  407. <label for="region">区域/位置(可选)</label>
  408. <input
  409. type="text"
  410. id="region"
  411. [(ngModel)]="newTask.region"
  412. [ngModelOptions]="{standalone: true}"
  413. placeholder="例如:客厅、主卧、厨房"
  414. class="ios-input"
  415. >
  416. </div>
  417. <div class="form-group">
  418. <label for="taskDeadline">截止时间 *</label>
  419. <!-- 显示当前选择的截止时间 -->
  420. <div class="deadline-display ios-input" (click)="deadlineDropdownVisible = !deadlineDropdownVisible">
  421. {{ getDisplayDeadline() || '请选择截止时间' }}
  422. <span class="dropdown-arrow" [class.rotate]="deadlineDropdownVisible">▼</span>
  423. </div>
  424. <!-- 预设时长下拉选择框 -->
  425. <div class="deadline-dropdown" [class.visible]="deadlineDropdownVisible">
  426. @for (preset of timePresets; track preset.hours) {
  427. <div class="dropdown-option"
  428. [class.selected]="selectedPreset === preset.hours.toString()"
  429. (click)="handlePresetSelection(preset.hours.toString())">
  430. {{ preset.label }}
  431. </div>
  432. }
  433. <!-- 当天24:00前选项 -->
  434. <div class="dropdown-option"
  435. [class.selected]="selectedPreset === 'today'"
  436. (click)="handlePresetSelection('today')">
  437. 今日24:00前
  438. </div>
  439. <!-- 自定义时间选项 -->
  440. <div class="dropdown-divider"></div>
  441. <div class="dropdown-option custom-option" (click)="handlePresetSelection('custom')">
  442. 自定义时间
  443. </div>
  444. </div>
  445. <!-- 错误提示信息 -->
  446. @if (deadlineError) {
  447. <div class="error-message">{{ deadlineError }}</div>
  448. }
  449. </div>
  450. <!-- 自定义时间选择弹窗 -->
  451. @if (isCustomTimeVisible) {
  452. <div class="custom-time-modal">
  453. <div class="modal-backdrop"></div>
  454. <div class="modal-content">
  455. <div class="modal-header">
  456. <h3>选择自定义时间</h3>
  457. <button class="close-button" (click)="closeCustomTimePicker()">×</button>
  458. </div>
  459. <div class="modal-body">
  460. <!-- 日期选择 -->
  461. <div class="date-picker">
  462. <label>日期</label>
  463. <input
  464. type="date"
  465. [(ngModel)]="customDate"
  466. min="{{ todayDate }}"
  467. max="{{ sevenDaysLaterDate }}"
  468. class="ios-input"
  469. />
  470. </div>
  471. <!-- 时间选择 -->
  472. <div class="time-picker">
  473. <label>时间</label>
  474. <input
  475. type="time"
  476. [(ngModel)]="customTime"
  477. class="ios-input"
  478. />
  479. </div>
  480. <!-- 错误提示信息 -->
  481. @if (deadlineError) {
  482. <div class="error-message">{{ deadlineError }}</div>
  483. }
  484. </div>
  485. <div class="modal-footer">
  486. <button class="cancel-button" (click)="closeCustomTimePicker()">取消</button>
  487. <button class="confirm-button" (click)="handleCustomTimeSelection()">确定</button>
  488. </div>
  489. </div>
  490. </div>
  491. }
  492. <div class="form-group">
  493. <label for="assigneeSelect">指派给(可选)</label>
  494. <select
  495. id="assigneeSelect"
  496. [(ngModel)]="newTask.assigneeId"
  497. [ngModelOptions]="{standalone: true}"
  498. class="ios-select"
  499. >
  500. <option value="">-- 暂不指派 --</option>
  501. @for (member of teamMembers(); track member.id) {
  502. <option [value]="member.id">{{ member.name }}{{ member.roleName ? ' (' + member.roleName + ')' : '' }}</option>
  503. }
  504. </select>
  505. </div>
  506. <div class="form-group">
  507. <label for="taskPriority">优先级 *</label>
  508. <select
  509. id="taskPriority"
  510. [(ngModel)]="newTask.priority"
  511. [ngModelOptions]="{standalone: true}"
  512. class="ios-select"
  513. >
  514. <option value="high">🔴 高优先级(紧急)</option>
  515. <option value="medium">🟡 中优先级(普通)</option>
  516. <option value="low">🟢 低优先级</option>
  517. </select>
  518. </div>
  519. <div class="form-group">
  520. <label for="taskDescription">详细描述(可选)</label>
  521. <textarea
  522. id="taskDescription"
  523. [(ngModel)]="newTask.description"
  524. [ngModelOptions]="{standalone: true}"
  525. placeholder="请详细描述需要紧急处理的问题..."
  526. rows="4"
  527. class="ios-textarea"
  528. ></textarea>
  529. </div>
  530. </form>
  531. </div>
  532. <div class="ios-panel-footer">
  533. <button type="button" class="ios-cancel-button" (click)="hideTaskForm()">取消</button>
  534. <button type="button" class="ios-submit-button" (click)="handleAddTaskSubmit()" [disabled]="isSubmitDisabled">确定</button>
  535. </div>
  536. </div>
  537. </div>
  538. }
  539. <!-- 项目动态流 -->
  540. <section class="project-updates-section">
  541. <div class="section-header">
  542. <h3>项目动态</h3>
  543. <div class="search-box">
  544. <input
  545. type="text"
  546. [value]="searchTerm()"
  547. (input)="searchTerm.set($any($event.target).value)"
  548. placeholder="搜索动态..."
  549. class="search-input"
  550. />
  551. </div>
  552. </div>
  553. <div class="updates-list">
  554. @if (filteredUpdates().length === 0) {
  555. <div class="empty-state">
  556. <svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  557. <circle cx="12" cy="12" r="10"></circle>
  558. <line x1="2" y1="12" x2="22" y2="12"></line>
  559. <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>
  560. </svg>
  561. <p>暂无项目动态</p>
  562. </div>
  563. }
  564. @for (update of filteredUpdates(); track update) {
  565. <div class="update-item">
  566. <div class="update-icon">
  567. <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  568. <path d="M22 12h-4l-3 9L9 3l-3 9H2"></path>
  569. </svg>
  570. </div>
  571. <div class="update-content">
  572. @if (isProjectUpdate(update) && update.name && update.status) {
  573. <div class="update-title">
  574. 项目 <strong>{{ update.name }}</strong> 状态更新为 {{ update.status }}
  575. </div>
  576. }
  577. @if (hasContent(update)) {
  578. <div class="update-title">
  579. <strong>{{ getCustomerName(update) }}</strong> 提交了反馈
  580. </div>
  581. }
  582. @if (hasContent(update) && getUpdateContent(update)) {
  583. <p class="update-text">{{ getUpdateContent(update) }}</p>
  584. }
  585. <div class="update-meta">
  586. <span class="update-time">{{ getFormattedDate(update) }}</span>
  587. <span class="update-status {{ getUpdateStatusClass(update) }}">
  588. {{ getUpdateStatus(update) }}
  589. </span>
  590. </div>
  591. </div>
  592. </div>
  593. }
  594. </div>
  595. </section>
  596. <!-- 回到顶部按钮 -->
  597. <button class="back-to-top" (click)="scrollToTop()" [class.visible]="showBackToTop()">
  598. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  599. <polyline points="18 15 12 9 6 15"></polyline>
  600. </svg>
  601. </button>