project-list.html 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. <div class="project-list-container">
  2. <!-- 右侧主要内容 -->
  3. <div class="project-content">
  4. <!-- 页面标题和操作 -->
  5. <div class="page-header">
  6. <h2>项目看板</h2>
  7. <div class="header-actions">
  8. <div class="search-container">
  9. <div class="search-box">
  10. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  11. <circle cx="11" cy="11" r="8"></circle>
  12. <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
  13. </svg>
  14. <input
  15. type="text"
  16. placeholder="搜索项目名称或客户..."
  17. [value]="searchTerm()"
  18. (input)="searchTerm.set($any($event.target).value)"
  19. (keyup.enter)="onSearch()"
  20. >
  21. <button class="search-btn" (click)="onSearch()">
  22. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  23. <line x1="5" y1="12" x2="19" y2="12"></line>
  24. <polyline points="12 5 19 12 12 19"></polyline>
  25. </svg>
  26. </button>
  27. </div>
  28. </div>
  29. <div class="view-toggle">
  30. <button [class.active]="viewMode() === 'card'" (click)="toggleView('card')">卡片</button>
  31. <button [class.active]="viewMode() === 'list'" (click)="toggleView('list')">列表</button>
  32. <button [class.active]="viewMode() === 'dashboard'" (click)="toggleView('dashboard')">监控大盘</button>
  33. </div>
  34. <button class="add-project-btn" (click)="navigateToCreateOrder()">
  35. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  36. <line x1="12" y1="5" x2="12" y2="19"></line>
  37. <line x1="5" y1="12" x2="19" y2="12"></line>
  38. </svg>
  39. 添加项目
  40. </button>
  41. </div>
  42. </div>
  43. <!-- 筛选和排序工具栏 -->
  44. <div class="filter-toolbar">
  45. <div class="filter-group">
  46. <label>状态筛选</label>
  47. <select (change)="onStatusChange($event)" [value]="statusFilter()">
  48. @for (option of statusOptions; track option.value) {
  49. <option [value]="option.value">{{ option.label }}</option>
  50. }
  51. </select>
  52. </div>
  53. <div class="filter-group">
  54. <label>阶段筛选</label>
  55. <select (change)="onStageChange($event)" [value]="stageFilter()">
  56. @for (option of stageOptions; track option.value) {
  57. <option [value]="option.value">{{ option.label }}</option>
  58. }
  59. </select>
  60. </div>
  61. <div class="filter-group">
  62. <label>排序方式</label>
  63. <select (change)="onSortChange($event)" [value]="sortBy()">
  64. @for (option of sortOptions; track option.value) {
  65. <option [value]="option.value">{{ option.label }}</option>
  66. }
  67. </select>
  68. </div>
  69. <div class="filter-results">
  70. <span>共 {{ projects().length }} 个项目</span>
  71. </div>
  72. </div>
  73. <!-- 加载状态 -->
  74. @if (isLoading()) {
  75. <div class="loading-container">
  76. <div class="loading-spinner"></div>
  77. <p>正在加载项目数据...</p>
  78. </div>
  79. }
  80. <!-- 错误状态 -->
  81. @if (loadError()) {
  82. <div class="error-container">
  83. <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  84. <circle cx="12" cy="12" r="10"></circle>
  85. <line x1="12" y1="8" x2="12" y2="12"></line>
  86. <line x1="12" y1="16" x2="12.01" y2="16"></line>
  87. </svg>
  88. <p>{{ loadError() }}</p>
  89. <button class="retry-btn" (click)="loadProjects()">重试</button>
  90. </div>
  91. }
  92. <!-- 视图:卡片模式(看板) -->
  93. @if (viewMode() === 'card' && !isLoading() && !loadError()) {
  94. <div class="kanban-container">
  95. <div class="kanban-scroll">
  96. <!-- 列头 -->
  97. <div class="kanban-header">
  98. @for (col of columns; track col.id) {
  99. <div class="kanban-column-header" [attr.data-col]="col.id">
  100. <h3 class="column-title">{{ col.name }}</h3>
  101. <span class="stage-count">{{ getProjectsByColumn(col.id).length }}</span>
  102. </div>
  103. }
  104. </div>
  105. <!-- 列体 -->
  106. <div class="kanban-body">
  107. @for (col of columns; track col.id) {
  108. <div class="kanban-column" [attr.data-col]="col.id">
  109. @for (project of getProjectsByColumn(col.id); track project.id) {
  110. <div class="kanban-card" (click)="navigateToProject(project, col.id)">
  111. <div class="kanban-card-header">
  112. <div class="left">
  113. <h4 class="project-name">{{ project.name }}</h4>
  114. <span class="project-id">#{{ project.id }}</span>
  115. </div>
  116. <div class="right">
  117. @if (col.id === 'order') {
  118. <span class="pending-badge">待分配</span>
  119. }
  120. <span class="project-tag">{{ project.tagDisplayText }}</span>
  121. @if (project.isUrgent) {
  122. <span class="urgent-tag">紧急</span>
  123. }
  124. </div>
  125. </div>
  126. <div class="kanban-card-content">
  127. <p class="customer">客户:{{ project.customerName }}</p>
  128. <p class="assignee">设计师:{{ project.assigneeName || '未分配' }}</p>
  129. <p class="stage">阶段:<span class="stage-badge" [class]="getStageClass(project.currentStage)">{{ project.currentStage }}</span></p>
  130. <div class="progress-line">
  131. <div class="progress-bar">
  132. <div class="progress-fill" [style.width.percent]="project.progress"></div>
  133. </div>
  134. <span class="progress-text">{{ project.progress }}%</span>
  135. </div>
  136. <p class="deadline" [class.overdue]="project.daysUntilDeadline < 0" [class.urgent]="project.isUrgent">
  137. 截止:{{ formatDate(project.deadline) }}
  138. </p>
  139. </div>
  140. <div class="kanban-card-footer">
  141. <button class="btn-link" (click)="$event.stopPropagation(); navigateToProject(project, col.id)">进入</button>
  142. <button class="btn-link" (click)="$event.stopPropagation(); navigateToMessages(project)">沟通管理</button>
  143. </div>
  144. </div>
  145. }
  146. @if (getProjectsByColumn(col.id).length === 0) {
  147. <div class="empty-column">
  148. <span class="empty-icon">📦</span>
  149. <p>暂无项目</p>
  150. </div>
  151. }
  152. </div>
  153. }
  154. </div>
  155. </div>
  156. </div>
  157. }
  158. <!-- 视图:列表模式(沿用原卡片列表 + 分页) -->
  159. @if (viewMode() === 'list') {
  160. <div class="project-grid">
  161. @for (project of paginatedProjects(); track project.id) {
  162. <div class="project-card">
  163. <div class="card-header">
  164. <div class="card-title-section">
  165. <h3 class="project-name">{{ project.name }}</h3>
  166. <span class="project-id">#{{ project.id }}</span>
  167. </div>
  168. <div class="card-tags">
  169. <span class="project-tag">{{ project.tagDisplayText }}</span>
  170. @if (project.isUrgent) { <span class="urgent-tag">紧急</span> }
  171. </div>
  172. </div>
  173. <div class="card-content">
  174. <div class="info-item">
  175. <span class="info-label">客户</span>
  176. <span class="info-value">{{ project.customerName }}</span>
  177. </div>
  178. <div class="info-item">
  179. <span class="info-label">设计师</span>
  180. <span class="info-value">{{ project.assigneeName || '未分配' }}</span>
  181. </div>
  182. <div class="info-item">
  183. <span class="info-label">状态</span>
  184. <span class="info-value status-badge" [class]="getStatusClass(project.status)">{{ project.status }}</span>
  185. </div>
  186. <div class="info-item">
  187. <span class="info-label">阶段</span>
  188. <span class="info-value stage-badge" [class]="getStageClass(project.currentStage)">{{ project.currentStage }}</span>
  189. </div>
  190. <div class="progress-section">
  191. <div class="progress-header">
  192. <span class="progress-label">进度</span>
  193. <span class="progress-percentage">{{ project.progress }}%</span>
  194. </div>
  195. <div class="progress-bar">
  196. <div class="progress-fill" [style.width.percent]="project.progress" [style.backgroundColor]="project.status === '进行中' ? '#1976d2' : '#757575'"></div>
  197. </div>
  198. </div>
  199. <div class="timeline-info">
  200. <div class="time-item">
  201. <span class="time-label">创建时间</span>
  202. <span class="time-value">{{ formatDate(project.createdAt) }}</span>
  203. </div>
  204. <div class="time-item">
  205. <span class="time-label">截止日期</span>
  206. <span class="time-value deadline" [class.overdue]="project.daysUntilDeadline < 0" [class.urgent]="project.isUrgent">
  207. {{ formatDate(project.deadline) }} ({{ project.daysUntilDeadline >= 0 ? '还有' + project.daysUntilDeadline + '天' : '已逾期' + getAbsValue(project.daysUntilDeadline) + '天' }})
  208. </span>
  209. </div>
  210. </div>
  211. @if (project.highPriorityNeeds && project.highPriorityNeeds.length > 0) {
  212. <div class="needs-section">
  213. <span class="needs-label">高优先级需求:</span>
  214. <ul class="needs-list">
  215. @for (need of project.highPriorityNeeds; track $index) {
  216. <li>
  217. <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  218. <circle cx="12" cy="12" r="10"></circle>
  219. <line x1="12" y1="8" x2="12" y2="12"></line>
  220. <line x1="12" y1="16" x2="12.01" y2="16"></line>
  221. </svg>
  222. {{ need }}
  223. </li>
  224. }
  225. </ul>
  226. </div>
  227. }
  228. </div>
  229. <div class="card-footer">
  230. <button class="secondary-btn card-action" (click)="navigateToMessages(project)">
  231. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  232. <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>
  233. </svg>
  234. <span>沟通管理</span>
  235. </button>
  236. <button class="primary-btn card-action" (click)="navigateToProject(project, getColumnIdForProject(project))">
  237. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  238. <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>
  239. </svg>
  240. <span>查看详情</span>
  241. </button>
  242. </div>
  243. </div>
  244. }
  245. </div>
  246. <!-- 分页控件 -->
  247. @if (totalPages() > 1) {
  248. <div class="pagination">
  249. <button class="pagination-btn" (click)="prevPage()" [disabled]="currentPage() === 1">
  250. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  251. <line x1="15" y1="18" x2="9" y2="12"></line>
  252. <line x1="9" y1="18" x2="15" y2="12"></line>
  253. </svg>
  254. </button>
  255. @for (page of pageNumbers(); track page) {
  256. <button class="pagination-btn" [class.active]="page === currentPage()" (click)="goToPage(page)">{{ page }}</button>
  257. }
  258. @if (totalPages() > 5) { <span class="pagination-ellipsis">...</span> }
  259. @if (totalPages() > 5) {
  260. <button class="pagination-btn" (click)="goToPage(totalPages())">{{ totalPages() }}</button>
  261. }
  262. <button class="pagination-btn" (click)="nextPage()" [disabled]="currentPage() === totalPages()">
  263. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  264. <line x1="9" y1="18" x2="15" y2="12"></line>
  265. <line x1="15" y1="6" x2="9" y2="12"></line>
  266. </svg>
  267. </button>
  268. </div>
  269. }
  270. }
  271. <!-- 视图:监控大盘模式 -->
  272. @if (viewMode() === 'dashboard') {
  273. <div class="dashboard-container">
  274. <!-- 使用iframe嵌入组长端监控大盘,通过CSS隐藏待办任务栏 -->
  275. <iframe
  276. src="/team-leader/dashboard"
  277. frameborder="0"
  278. width="100%"
  279. height="100%"
  280. style="min-height: 800px;"
  281. title="项目监控大盘"
  282. onload="const doc = this.contentDocument; doc.querySelector('.todo-section').style.display = 'none'; const header = doc.querySelector('.dashboard-header h1'); if (header && header.textContent.includes('设计组长工作台')) { header.style.display = 'none'; doc.querySelector('.dashboard-metrics').style.marginTop = '20px'; }">
  283. </iframe>
  284. </div>
  285. }
  286. </div>