dashboard.html 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. <div class="admin-dashboard">
  2. <!-- 页面标题 -->
  3. <div class="page-header">
  4. <h2 class="page-title">总览看板</h2>
  5. <p class="page-description">系统运营数据总览和趋势分析</p>
  6. </div>
  7. <!-- 统计卡片区域 -->
  8. <div class="stats-grid">
  9. <!-- 总项目数 -->
  10. <div class="stat-card clickable" (click)="showPanel('totalProjects')">
  11. <div class="stat-icon primary">
  12. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  13. <line x1="8" y1="6" x2="21" y2="6"></line>
  14. <line x1="8" y1="12" x2="21" y2="12"></line>
  15. <line x1="8" y1="18" x2="21" y2="18"></line>
  16. <line x1="3" y1="6" x2="3.01" y2="6"></line>
  17. <line x1="3" y1="12" x2="3.01" y2="12"></line>
  18. <line x1="3" y1="18" x2="3.01" y2="18"></line>
  19. </svg>
  20. </div>
  21. <div class="stat-content">
  22. <div class="stat-value">{{ stats.totalProjects() }}</div>
  23. <div class="stat-label">总项目数</div>
  24. </div>
  25. <div class="stat-trend positive">
  26. <span>+12%</span>
  27. </div>
  28. </div>
  29. <!-- 进行中项目 -->
  30. <div class="stat-card clickable" (click)="showPanel('active')">
  31. <div class="stat-icon secondary">
  32. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  33. <path d="M22 12h-4l-3 9L9 3l-3 9H2"></path>
  34. </svg>
  35. </div>
  36. <div class="stat-content">
  37. <div class="stat-value">{{ stats.activeProjects() }}</div>
  38. <div class="stat-label">进行中项目</div>
  39. </div>
  40. <div class="stat-trend positive">
  41. <span>+8%</span>
  42. </div>
  43. </div>
  44. <!-- 已完成项目 -->
  45. <div class="stat-card clickable" (click)="showPanel('completed')">
  46. <div class="stat-icon success">
  47. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  48. <polyline points="20 6 9 17 4 12"></polyline>
  49. </svg>
  50. </div>
  51. <div class="stat-content">
  52. <div class="stat-value">{{ stats.completedProjects() }}</div>
  53. <div class="stat-label">已完成项目</div>
  54. </div>
  55. <div class="stat-trend positive">
  56. <span>+15%</span>
  57. </div>
  58. </div>
  59. <!-- 设计师总数 -->
  60. <div class="stat-card clickable" (click)="showPanel('designers')">
  61. <div class="stat-icon primary">
  62. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  63. <path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
  64. <circle cx="9" cy="7" r="4"></circle>
  65. <path d="M22 21v-2a4 4 0 0 0-3-3.87"></path>
  66. <path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
  67. </svg>
  68. </div>
  69. <div class="stat-content">
  70. <div class="stat-value">{{ stats.totalDesigners() }}</div>
  71. <div class="stat-label">设计师总数</div>
  72. </div>
  73. <div class="stat-trend neutral">
  74. <span>持平</span>
  75. </div>
  76. </div>
  77. <!-- 客户总数 -->
  78. <div class="stat-card clickable" (click)="showPanel('customers')">
  79. <div class="stat-icon secondary">
  80. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  81. <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
  82. <circle cx="12" cy="7" r="4"></circle>
  83. </svg>
  84. </div>
  85. <div class="stat-content">
  86. <div class="stat-value">{{ stats.totalCustomers() }}</div>
  87. <div class="stat-label">客户总数</div>
  88. </div>
  89. <div class="stat-trend positive">
  90. <span>+20%</span>
  91. </div>
  92. </div>
  93. <!-- 总收入 -->
  94. <div class="stat-card clickable" (click)="showPanel('revenue')">
  95. <div class="stat-icon success">
  96. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  97. <line x1="12" y1="1" x2="12" y2="23"></line>
  98. <path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>
  99. </svg>
  100. </div>
  101. <div class="stat-content">
  102. <div class="stat-value">{{ formatCurrency(stats.totalRevenue()) }}</div>
  103. <div class="stat-label">总收入</div>
  104. </div>
  105. <div class="stat-trend positive">
  106. <span>+28%</span>
  107. </div>
  108. </div>
  109. </div>
  110. <!-- 图表区域 -->
  111. <div class="charts-grid">
  112. <!-- 项目趋势图 -->
  113. <div class="chart-card">
  114. <div class="chart-header">
  115. <h3>项目数量趋势</h3>
  116. <div class="chart-period">
  117. <button class="period-btn" [class.active]="projectPeriod() === '6m'" (click)="setProjectPeriod('6m')">近6个月</button>
  118. <button class="period-btn" [class.active]="projectPeriod() === '12m'" (click)="setProjectPeriod('12m')">近12个月</button>
  119. </div>
  120. </div>
  121. <div id="projectTrendChart" class="chart-container"></div>
  122. </div>
  123. <!-- 收入统计图 -->
  124. <div class="chart-card">
  125. <div class="chart-header">
  126. <h3>季度收入统计</h3>
  127. <div class="chart-period">
  128. <button class="period-btn" [class.active]="revenuePeriod() === 'quarter'" (click)="setRevenuePeriod('quarter')">本季度</button>
  129. <button class="period-btn" [class.active]="revenuePeriod() === 'year'" (click)="setRevenuePeriod('year')">全年</button>
  130. </div>
  131. </div>
  132. <div id="revenueChart" class="chart-container"></div>
  133. </div>
  134. </div>
  135. <!-- 最近活动 - 重新设计 -->
  136. <div class="recent-activities-enhanced">
  137. <div class="section-header-enhanced">
  138. <div class="header-left">
  139. <div class="header-icon">
  140. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  141. <polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline>
  142. </svg>
  143. </div>
  144. <div class="header-text">
  145. <h3>最近活动</h3>
  146. <p class="header-subtitle">实时跟踪系统动态 • 共 {{ recentActivities().length }} 条</p>
  147. </div>
  148. </div>
  149. <button class="view-all-btn" (click)="viewAllActivities()" *ngIf="!showAllActivities()">
  150. <span>查看全部</span>
  151. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  152. <line x1="5" y1="12" x2="19" y2="12"></line>
  153. <polyline points="12 5 19 12 12 19"></polyline>
  154. </svg>
  155. </button>
  156. </div>
  157. <!-- 加载状态 -->
  158. @if (loadingActivities()) {
  159. <div class="loading-activities">
  160. <div class="spinner"></div>
  161. <p>加载活动日志...</p>
  162. </div>
  163. }
  164. <!-- 活动列表 -->
  165. @if (!loadingActivities() && recentActivities().length > 0) {
  166. <div class="timeline-container">
  167. @for (activity of displayedActivities(); track activity.id; let isLast = $last) {
  168. <div class="timeline-item" [ngClass]="getActivityClass(activity)">
  169. <div class="timeline-marker">
  170. <div class="marker-dot"></div>
  171. @if (!isLast) {
  172. <div class="marker-line"></div>
  173. }
  174. </div>
  175. <div class="timeline-content">
  176. <div class="activity-card">
  177. <div class="card-header">
  178. <div class="activity-type-badge" [ngClass]="getActivityClass(activity)">
  179. <span [innerHTML]="activity.icon"></span>
  180. <span>{{ getActionTypeLabel(activity.actionType) }}</span>
  181. </div>
  182. <div class="activity-time">
  183. <svg width="12" height="12" 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. <span>{{ activity.formattedTime }}</span>
  188. </div>
  189. </div>
  190. <div class="card-body">
  191. <div class="activity-description">
  192. <span class="actor-name">{{ activity.actorName }}</span> {{ activity.description }}
  193. <div [ngClass]="getEntityTagClass(activity.module)">
  194. <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  195. @if (activity.module === 'project') {
  196. <path d="M3 3h7v7H3z"></path>
  197. <path d="M14 3h7v7h-7z"></path>
  198. <path d="M14 14h7v7h-7z"></path>
  199. <path d="M3 14h7v7H3z"></path>
  200. }
  201. @if (activity.module === 'customer') {
  202. <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
  203. <circle cx="12" cy="7" r="4"></circle>
  204. }
  205. @if (activity.module === 'task' || activity.module === 'design' || activity.module === 'urgent_task') {
  206. <polyline points="9 11 12 14 22 4"></polyline>
  207. <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
  208. }
  209. @if (activity.module === 'finance') {
  210. <line x1="12" y1="1" x2="12" y2="23"></line>
  211. <path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>
  212. }
  213. </svg>
  214. <span>{{ activity.entityName }}</span>
  215. </div>
  216. </div>
  217. </div>
  218. </div>
  219. </div>
  220. </div>
  221. }
  222. </div>
  223. }
  224. <!-- 空状态 -->
  225. @if (!loadingActivities() && recentActivities().length === 0) {
  226. <div class="empty-activities">
  227. <svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  228. <circle cx="12" cy="12" r="10"></circle>
  229. <line x1="12" y1="8" x2="12" y2="12"></line>
  230. <line x1="12" y1="16" x2="12.01" y2="16"></line>
  231. </svg>
  232. <p>暂无活动记录</p>
  233. </div>
  234. }
  235. </div>
  236. <!-- 详情抽屉 -->
  237. <div class="drawer" *ngIf="detailOpen()" (click)="closeDetailPanel()">
  238. <div class="drawer-panel" (click)="$event.stopPropagation()">
  239. <div class="drawer-header">
  240. <h3 class="drawer-title">{{ detailTitle() }}</h3>
  241. <button class="drawer-close" (click)="closeDetailPanel()">✕</button>
  242. </div>
  243. <div class="drawer-body">
  244. <div id="detailChart" class="chart-container"></div>
  245. <div class="drawer-toolbar">
  246. <div class="filters">
  247. <input type="text" placeholder="关键字搜索"
  248. [value]="keyword()"
  249. (input)="setKeyword($any($event.target).value)" />
  250. <select [value]="statusFilter()" (change)="setStatus($any($event.target).value)">
  251. <option *ngFor="let opt of getStatusOptions()" [value]="opt.value">{{ opt.label }}</option>
  252. </select>
  253. <input type="date" [value]="dateFrom() || ''" (change)="setDateFrom($any($event.target).value)" />
  254. <span class="date-sep">-</span>
  255. <input type="date" [value]="dateTo() || ''" (change)="setDateTo($any($event.target).value)" />
  256. <button class="btn reset" (click)="resetFilters()">重置</button>
  257. </div>
  258. <div class="actions">
  259. <button class="btn export" (click)="exportCSV()">导出 CSV</button>
  260. </div>
  261. </div>
  262. <div class="detail-table-wrapper">
  263. <table class="detail-table">
  264. <thead>
  265. <tr>
  266. <th *ngFor="let col of getColumns()">{{ col.label }}</th>
  267. </tr>
  268. </thead>
  269. <tbody>
  270. <tr *ngFor="let row of pagedData(); let i = index">
  271. <td *ngFor="let col of getColumns()">{{ col.formatter ? col.formatter(row[col.field]) : row[col.field] }}</td>
  272. </tr>
  273. <tr *ngIf="pagedData().length === 0">
  274. <td class="empty" [attr.colspan]="getColumns().length">暂无数据</td>
  275. </tr>
  276. </tbody>
  277. </table>
  278. <div class="pagination">
  279. <div class="info">共 {{ totalItems() }} 条 • 第 {{ pageIndex() }} / {{ totalPages }} 页</div>
  280. <div class="pager">
  281. <button (click)="prevPage()" [disabled]="pageIndex() <= 1">上一页</button>
  282. <ng-container *ngFor="let n of getPages()">
  283. <button class="num" [class.active]="pageIndex() === n" (click)="goToPage(n)">{{ n }}</button>
  284. </ng-container>
  285. <button (click)="nextPage()" [disabled]="pageIndex() >= totalPages">下一页</button>
  286. </div>
  287. </div>
  288. </div>
  289. </div>
  290. </div>
  291. </div>
  292. </div>