data-reports.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  1. import { Component, OnInit } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { RouterModule, Router } from '@angular/router';
  4. import { FormsModule } from '@angular/forms';
  5. import * as XLSX from 'xlsx';
  6. import jsPDF from 'jspdf';
  7. import autoTable from 'jspdf-autotable';
  8. interface LocationData {
  9. id: string;
  10. name: string;
  11. type: 'community' | 'mall' | 'school' | 'office';
  12. wasteVolume: number;
  13. recycleVolume: number;
  14. capacity: number;
  15. status: 'normal' | 'warning' | 'critical';
  16. lastUpdate: string;
  17. }
  18. interface ChartData {
  19. label: string;
  20. value: number;
  21. color?: string;
  22. }
  23. interface StatCard {
  24. title: string;
  25. value: string;
  26. unit: string;
  27. trend: number;
  28. icon: string;
  29. color: string;
  30. }
  31. interface TrendData {
  32. date: string;
  33. value: number;
  34. }
  35. @Component({
  36. selector: 'app-data-reports',
  37. standalone: true,
  38. imports: [CommonModule, RouterModule, FormsModule],
  39. templateUrl: './data-reports.html',
  40. styleUrl: './data-reports.scss'
  41. })
  42. export class DataReports implements OnInit {
  43. reportType: 'business' | 'environmental' | 'government' = 'business';
  44. timeRange: 'today' | 'week' | 'month' | 'year' = 'month';
  45. showMonitorModal = false;
  46. currentNav = 'reports'; // 当前导航项
  47. showCompareModal = false; // 数据对比弹窗
  48. compareTimeRange: 'today' | 'week' | 'month' | 'year' = 'week'; // 对比时间范围
  49. // 地点监控数据
  50. locations: LocationData[] = [
  51. {
  52. id: 'LOC-001',
  53. name: '望京社区A区',
  54. type: 'community',
  55. wasteVolume: 850,
  56. recycleVolume: 680,
  57. capacity: 1000,
  58. status: 'warning',
  59. lastUpdate: '2分钟前'
  60. },
  61. {
  62. id: 'LOC-002',
  63. name: '朝阳大悦城',
  64. type: 'mall',
  65. wasteVolume: 920,
  66. recycleVolume: 750,
  67. capacity: 1000,
  68. status: 'critical',
  69. lastUpdate: '5分钟前'
  70. },
  71. {
  72. id: 'LOC-003',
  73. name: '中关村创业大街',
  74. type: 'office',
  75. wasteVolume: 450,
  76. recycleVolume: 380,
  77. capacity: 1000,
  78. status: 'normal',
  79. lastUpdate: '10分钟前'
  80. },
  81. {
  82. id: 'LOC-004',
  83. name: '北京实验学校',
  84. type: 'school',
  85. wasteVolume: 680,
  86. recycleVolume: 580,
  87. capacity: 1000,
  88. status: 'normal',
  89. lastUpdate: '15分钟前'
  90. },
  91. {
  92. id: 'LOC-005',
  93. name: '国贸CBD商圈',
  94. type: 'mall',
  95. wasteVolume: 950,
  96. recycleVolume: 820,
  97. capacity: 1000,
  98. status: 'critical',
  99. lastUpdate: '3分钟前'
  100. },
  101. {
  102. id: 'LOC-006',
  103. name: '海淀区社区B区',
  104. type: 'community',
  105. wasteVolume: 720,
  106. recycleVolume: 600,
  107. capacity: 1000,
  108. status: 'normal',
  109. lastUpdate: '8分钟前'
  110. }
  111. ];
  112. ngOnInit() {
  113. // 初始化数据
  114. }
  115. // 根据时间范围获取统计卡片数据
  116. get statsCards(): StatCard[] {
  117. const dataMap = {
  118. today: [
  119. { title: '今日回收量', value: '2.8', unit: '吨', trend: 12, icon: '📦', color: '#4CAF50' },
  120. { title: '今日收入', value: '15,680', unit: '元', trend: 8, icon: '💰', color: '#2196F3' },
  121. { title: '今日订单', value: '156', unit: '单', trend: -3, icon: '📋', color: '#FF9800' },
  122. { title: '今日用户', value: '89', unit: '人', trend: 15, icon: '👥', color: '#9C27B0' }
  123. ],
  124. week: [
  125. { title: '本周回收量', value: '18.5', unit: '吨', trend: 18, icon: '📦', color: '#4CAF50' },
  126. { title: '本周收入', value: '98,450', unit: '元', trend: 22, icon: '💰', color: '#2196F3' },
  127. { title: '本周订单', value: '1,024', unit: '单', trend: 12, icon: '📋', color: '#FF9800' },
  128. { title: '本周用户', value: '567', unit: '人', trend: 28, icon: '👥', color: '#9C27B0' }
  129. ],
  130. month: [
  131. { title: '本月回收量', value: '75.2', unit: '吨', trend: 25, icon: '📦', color: '#4CAF50' },
  132. { title: '本月收入', value: '425,800', unit: '元', trend: 32, icon: '💰', color: '#2196F3' },
  133. { title: '本月订单', value: '4,386', unit: '单', trend: 18, icon: '📋', color: '#FF9800' },
  134. { title: '本月用户', value: '2,145', unit: '人', trend: 42, icon: '👥', color: '#9C27B0' }
  135. ],
  136. year: [
  137. { title: '本年回收量', value: '856', unit: '吨', trend: 38, icon: '📦', color: '#4CAF50' },
  138. { title: '本年收入', value: '4.85', unit: '百万', trend: 45, icon: '💰', color: '#2196F3' },
  139. { title: '本年订单', value: '48,562', unit: '单', trend: 35, icon: '📋', color: '#FF9800' },
  140. { title: '本年用户', value: '18,945', unit: '人', trend: 52, icon: '👥', color: '#9C27B0' }
  141. ]
  142. };
  143. return dataMap[this.timeRange];
  144. }
  145. // 根据时间范围获取趋势数据
  146. get trendData(): TrendData[] {
  147. const dataMap = {
  148. today: [
  149. { date: '00:00', value: 120 },
  150. { date: '04:00', value: 80 },
  151. { date: '08:00', value: 280 },
  152. { date: '12:00', value: 450 },
  153. { date: '16:00', value: 380 },
  154. { date: '20:00', value: 320 },
  155. { date: '23:59', value: 280 }
  156. ],
  157. week: [
  158. { date: '周一', value: 2100 },
  159. { date: '周二', value: 2450 },
  160. { date: '周三', value: 2800 },
  161. { date: '周四', value: 2650 },
  162. { date: '周五', value: 3100 },
  163. { date: '周六', value: 2900 },
  164. { date: '周日', value: 2500 }
  165. ],
  166. month: [
  167. { date: '1-5日', value: 12500 },
  168. { date: '6-10日', value: 14200 },
  169. { date: '11-15日', value: 15800 },
  170. { date: '16-20日', value: 16500 },
  171. { date: '21-25日', value: 17200 },
  172. { date: '26-30日', value: 18600 }
  173. ],
  174. year: [
  175. { date: '1月', value: 65000 },
  176. { date: '2月', value: 58000 },
  177. { date: '3月', value: 72000 },
  178. { date: '4月', value: 68000 },
  179. { date: '5月', value: 75000 },
  180. { date: '6月', value: 82000 },
  181. { date: '7月', value: 88000 },
  182. { date: '8月', value: 85000 },
  183. { date: '9月', value: 78000 },
  184. { date: '10月', value: 92000 },
  185. { date: '11月', value: 0 },
  186. { date: '12月', value: 0 }
  187. ]
  188. };
  189. return dataMap[this.timeRange];
  190. }
  191. // 获取品类分布数据(根据时间范围)
  192. get categoryData(): ChartData[] {
  193. const dataMap = {
  194. today: [
  195. { label: '纸类', value: 980, color: '#3498db' },
  196. { label: '塑料', value: 750, color: '#2ecc71' },
  197. { label: '金属', value: 520, color: '#f39c12' },
  198. { label: '玻璃', value: 380, color: '#9b59b6' },
  199. { label: '其他', value: 170, color: '#95a5a6' }
  200. ],
  201. week: [
  202. { label: '纸类', value: 6500, color: '#3498db' },
  203. { label: '塑料', value: 5200, color: '#2ecc71' },
  204. { label: '金属', value: 3600, color: '#f39c12' },
  205. { label: '玻璃', value: 2100, color: '#9b59b6' },
  206. { label: '其他', value: 1100, color: '#95a5a6' }
  207. ],
  208. month: [
  209. { label: '纸类', value: 26300, color: '#3498db' },
  210. { label: '塑料', value: 21000, color: '#2ecc71' },
  211. { label: '金属', value: 13500, color: '#f39c12' },
  212. { label: '玻璃', value: 9000, color: '#9b59b6' },
  213. { label: '其他', value: 5400, color: '#95a5a6' }
  214. ],
  215. year: [
  216. { label: '纸类', value: 299600, color: '#3498db' },
  217. { label: '塑料', value: 239700, color: '#2ecc71' },
  218. { label: '金属', value: 154100, color: '#f39c12' },
  219. { label: '玻璃', value: 102700, color: '#9b59b6' },
  220. { label: '其他', value: 59900, color: '#95a5a6' }
  221. ]
  222. };
  223. return dataMap[this.timeRange];
  224. }
  225. // 获取环保数据
  226. get environmentalData(): ChartData[] {
  227. const dataMap = {
  228. today: [
  229. { label: '碳减排', value: 42, color: '#2ecc71' },
  230. { label: '节水量', value: 156, color: '#3498db' },
  231. { label: '节电量', value: 238, color: '#f39c12' }
  232. ],
  233. week: [
  234. { label: '碳减排', value: 285, color: '#2ecc71' },
  235. { label: '节水量', value: 1050, color: '#3498db' },
  236. { label: '节电量', value: 1620, color: '#f39c12' }
  237. ],
  238. month: [
  239. { label: '碳减排', value: 1250, color: '#2ecc71' },
  240. { label: '节水量', value: 4600, color: '#3498db' },
  241. { label: '节电量', value: 7100, color: '#f39c12' }
  242. ],
  243. year: [
  244. { label: '碳减排', value: 14250, color: '#2ecc71' },
  245. { label: '节水量', value: 52400, color: '#3498db' },
  246. { label: '节电量', value: 80900, color: '#f39c12' }
  247. ]
  248. };
  249. return dataMap[this.timeRange];
  250. }
  251. // 获取时间范围标题
  252. get timeRangeTitle(): string {
  253. const titleMap = {
  254. today: '今日',
  255. week: '本周',
  256. month: '本月',
  257. year: '本年'
  258. };
  259. return titleMap[this.timeRange];
  260. }
  261. get criticalLocations(): LocationData[] {
  262. return this.locations.filter(loc => loc.status === 'critical');
  263. }
  264. get warningLocations(): LocationData[] {
  265. return this.locations.filter(loc => loc.status === 'warning');
  266. }
  267. getLocationIcon(type: string): string {
  268. const icons: {[key: string]: string} = {
  269. 'community': '🏘️',
  270. 'mall': '🏬',
  271. 'school': '🏫',
  272. 'office': '🏢'
  273. };
  274. return icons[type] || '📍';
  275. }
  276. getStatusText(status: string): string {
  277. const statusMap: {[key: string]: string} = {
  278. 'normal': '正常',
  279. 'warning': '预警',
  280. 'critical': '严重'
  281. };
  282. return statusMap[status] || status;
  283. }
  284. getStatusClass(status: string): string {
  285. return `status-${status}`;
  286. }
  287. getCapacityPercent(location: LocationData): number {
  288. return Math.round((location.wasteVolume / location.capacity) * 100);
  289. }
  290. // 计算品类占比
  291. getCategoryPercent(value: number): number {
  292. const total = this.categoryData.reduce((sum, item) => sum + item.value, 0);
  293. return Math.round((value / total) * 100);
  294. }
  295. // 获取趋势最大值(用于图表缩放)
  296. get maxTrendValue(): number {
  297. return Math.max(...this.trendData.map(d => d.value));
  298. }
  299. // 计算趋势图高度百分比
  300. getTrendHeight(value: number): number {
  301. return (value / this.maxTrendValue) * 100;
  302. }
  303. openMonitorView(): void {
  304. this.showMonitorModal = true;
  305. }
  306. closeMonitorView(): void {
  307. this.showMonitorModal = false;
  308. }
  309. dispatchWorkers(location: LocationData): void {
  310. alert(`正在为 ${location.name} 派遣回收人员...\n当前垃圾量: ${location.wasteVolume}kg`);
  311. }
  312. exportReport(format: string): void {
  313. if (format === 'excel') {
  314. this.exportToExcel();
  315. } else if (format === 'pdf') {
  316. this.exportToPDF();
  317. } else if (format === 'gov') {
  318. this.exportGovReport();
  319. }
  320. }
  321. // 导出Excel
  322. private exportToExcel(): void {
  323. const ws_data = [
  324. ['数据报表 - ' + this.timeRangeTitle],
  325. [],
  326. ['统计指标', '数值', '单位', '趋势'],
  327. ...this.statsCards.map(card => [card.title, card.value, card.unit, `${card.trend > 0 ? '+' : ''}${card.trend}%`]),
  328. [],
  329. ['品类分布', '重量', '占比'],
  330. ...this.categoryData.map(item => [item.label, `${item.value}kg`, `${this.getCategoryPercent(item.value)}%`]),
  331. [],
  332. ['环保数据', '数值', '单位'],
  333. ...this.environmentalData.map(item => {
  334. const unit = item.label === '碳减排' ? 'kg CO₂' : item.label === '节水量' ? '升' : '度';
  335. return [item.label, item.value, unit];
  336. })
  337. ];
  338. const ws = XLSX.utils.aoa_to_sheet(ws_data);
  339. const wb = XLSX.utils.book_new();
  340. XLSX.utils.book_append_sheet(wb, ws, '数据报表');
  341. const fileName = `数据报表_${this.timeRangeTitle}_${new Date().getTime()}.xlsx`;
  342. XLSX.writeFile(wb, fileName);
  343. alert('Excel报表导出成功!');
  344. }
  345. // 导出PDF
  346. private exportToPDF(): void {
  347. const doc = new jsPDF();
  348. // 添加标题(使用英文字体,避免中文乱码)
  349. doc.setFontSize(16);
  350. doc.text('Data Report - ' + this.timeRangeTitle, 14, 20);
  351. // 统计卡片数据
  352. const statsData = this.statsCards.map(card => [
  353. card.title,
  354. card.value + ' ' + card.unit,
  355. (card.trend > 0 ? '+' : '') + card.trend + '%'
  356. ]);
  357. autoTable(doc, {
  358. head: [['Statistic', 'Value', 'Trend']],
  359. body: statsData,
  360. startY: 30,
  361. theme: 'grid'
  362. });
  363. // 品类分布数据
  364. const categoryDataTable = this.categoryData.map(item => [
  365. item.label,
  366. item.value + 'kg',
  367. this.getCategoryPercent(item.value) + '%'
  368. ]);
  369. autoTable(doc, {
  370. head: [['Category', 'Weight', 'Percent']],
  371. body: categoryDataTable,
  372. startY: (doc as any).lastAutoTable.finalY + 10,
  373. theme: 'grid'
  374. });
  375. // 环保数据
  376. const envData = this.environmentalData.map(item => {
  377. const unit = item.label === '碳减排' ? 'kg CO2' : item.label === '节水量' ? 'Liter' : 'kWh';
  378. return [item.label, item.value + '', unit];
  379. });
  380. autoTable(doc, {
  381. head: [['Environmental', 'Value', 'Unit']],
  382. body: envData,
  383. startY: (doc as any).lastAutoTable.finalY + 10,
  384. theme: 'grid'
  385. });
  386. const fileName = `report_${this.timeRangeTitle}_${new Date().getTime()}.pdf`;
  387. doc.save(fileName);
  388. alert('PDF报表导出成功!');
  389. }
  390. // 导出政府申报表
  391. private exportGovReport(): void {
  392. const ws_data = [
  393. ['政府申报报表 - ' + this.timeRangeTitle],
  394. [],
  395. ['申报项目', '数值'],
  396. ['回收总量', this.statsCards[0].value + this.statsCards[0].unit],
  397. ['处理率', '95.8%'],
  398. ['合规率', '100%'],
  399. ['环保贡献(碳减排)', this.environmentalData[0].value + 'kg CO₂'],
  400. [],
  401. ['品类明细', '重量(kg)', '占比'],
  402. ...this.categoryData.map(item => [item.label, item.value, this.getCategoryPercent(item.value) + '%']),
  403. [],
  404. ['环保数据明细', '数值', '单位'],
  405. ...this.environmentalData.map(item => {
  406. const unit = item.label === '碳减排' ? 'kg CO₂' : item.label === '节水量' ? '升' : '度';
  407. return [item.label, item.value, unit];
  408. }),
  409. [],
  410. ['生成时间', new Date().toLocaleString('zh-CN')]
  411. ];
  412. const ws = XLSX.utils.aoa_to_sheet(ws_data);
  413. const wb = XLSX.utils.book_new();
  414. XLSX.utils.book_append_sheet(wb, ws, '政府申报');
  415. const fileName = `政府申报_${this.timeRangeTitle}_${new Date().getTime()}.xlsx`;
  416. XLSX.writeFile(wb, fileName);
  417. alert('政府申报表导出成功!');
  418. }
  419. submitToGov(): void {
  420. if (confirm(`确定要提交${this.timeRangeTitle}政府申报报表吗?`)) {
  421. alert('报表已成功提交!');
  422. }
  423. }
  424. // 计算品类总重量(吨)
  425. get totalCategoryWeight(): number {
  426. const total = this.categoryData.reduce((sum, item) => sum + item.value, 0);
  427. return Math.round(total / 100) / 10; // 转换为吨,保留1位小数
  428. }
  429. // 计算加工服务收入
  430. get processingIncome(): string {
  431. const baseIncome = parseFloat(this.statsCards[1].value.replace(/,/g, ''));
  432. return Math.round(baseIncome * 0.27).toLocaleString();
  433. }
  434. // 计算其他收入
  435. get otherIncome(): string {
  436. const baseIncome = parseFloat(this.statsCards[1].value.replace(/,/g, ''));
  437. return Math.round(baseIncome * 0.10).toLocaleString();
  438. }
  439. // Math辅助方法供模板使用
  440. Math = Math;
  441. // 导航方法
  442. navigateTo(path: string): void {
  443. this.router.navigate([path]);
  444. }
  445. // 刷新数据
  446. refreshData(): void {
  447. // 添加刷新动画效果
  448. const btn = event?.target as HTMLElement;
  449. const icon = btn.querySelector('.btn-icon') || btn;
  450. icon.classList.add('rotating');
  451. setTimeout(() => {
  452. alert('数据已刷新!最新数据已加载。');
  453. icon.classList.remove('rotating');
  454. }, 1000);
  455. }
  456. // 切换对比模式
  457. toggleCompare(): void {
  458. this.showCompareModal = true;
  459. }
  460. // 关闭对比弹窗
  461. closeCompareModal(): void {
  462. this.showCompareModal = false;
  463. }
  464. // 获取对比数据的统计卡片
  465. getCompareStatsCards(range: 'today' | 'week' | 'month' | 'year'): any[] {
  466. const dataMap = {
  467. today: [
  468. { title: '今日回收量', value: '2.8', unit: '吨', trend: 12 },
  469. { title: '今日收入', value: '15,680', unit: '元', trend: 8 },
  470. { title: '今日订单', value: '156', unit: '单', trend: -3 },
  471. { title: '今日用户', value: '89', unit: '人', trend: 15 }
  472. ],
  473. week: [
  474. { title: '本周回收量', value: '18.5', unit: '吨', trend: 18 },
  475. { title: '本周收入', value: '98,450', unit: '元', trend: 22 },
  476. { title: '本周订单', value: '1,024', unit: '单', trend: 12 },
  477. { title: '本周用户', value: '567', unit: '人', trend: 28 }
  478. ],
  479. month: [
  480. { title: '本月回收量', value: '75.2', unit: '吨', trend: 25 },
  481. { title: '本月收入', value: '425,800', unit: '元', trend: 32 },
  482. { title: '本月订单', value: '4,386', unit: '单', trend: 18 },
  483. { title: '本月用户', value: '2,145', unit: '人', trend: 42 }
  484. ],
  485. year: [
  486. { title: '本年回收量', value: '856', unit: '吨', trend: 38 },
  487. { title: '本年收入', value: '4.85', unit: '百万', trend: 45 },
  488. { title: '本年订单', value: '48,562', unit: '单', trend: 35 },
  489. { title: '本年用户', value: '18,945', unit: '人', trend: 52 }
  490. ]
  491. };
  492. return dataMap[range];
  493. }
  494. // 辅助方法:将字符串转为数字(移除逗号)
  495. parseValue(value: string): number {
  496. return parseFloat(value.replace(/,/g, ''));
  497. }
  498. // 辅助方法:比较两个值的大小
  499. isGreater(val1: string, val2: string): boolean {
  500. return this.parseValue(val1) > this.parseValue(val2);
  501. }
  502. isLess(val1: string, val2: string): boolean {
  503. return this.parseValue(val1) < this.parseValue(val2);
  504. }
  505. isEqual(val1: string, val2: string): boolean {
  506. return this.parseValue(val1) === this.parseValue(val2);
  507. }
  508. // 辅助方法:获取差值的箭头
  509. getDifferenceArrow(currentVal: string, compareVal: string): string {
  510. if (this.isGreater(currentVal, compareVal)) return '↑';
  511. if (this.isLess(currentVal, compareVal)) return '↓';
  512. return '→';
  513. }
  514. // 辅助方法:获取差值
  515. getDifference(currentVal: string, compareVal: string): string {
  516. const diff = Math.abs(this.parseValue(currentVal) - this.parseValue(compareVal));
  517. return diff.toLocaleString();
  518. }
  519. // 辅助方法:获取增长率
  520. getGrowthRate(currentVal: string, compareVal: string): string {
  521. const current = this.parseValue(currentVal);
  522. const compare = this.parseValue(compareVal);
  523. if (compare === 0) return '0.0';
  524. const rate = ((current - compare) / compare * 100);
  525. return Math.abs(rate).toFixed(1);
  526. }
  527. // 辅助方法:获取下降率
  528. getDeclineRate(currentVal: string, compareVal: string): string {
  529. const current = this.parseValue(currentVal);
  530. const compare = this.parseValue(compareVal);
  531. if (compare === 0) return '0.0';
  532. const rate = ((compare - current) / compare * 100);
  533. return Math.abs(rate).toFixed(1);
  534. }
  535. // 打开AI分析
  536. openAIAnalysis(): void {
  537. if (confirm('是否跳转到AI运营助手进行智能分析?')) {
  538. this.router.navigate(['/business/ai-operations-assistant']);
  539. }
  540. }
  541. constructor(private router: Router) {}
  542. }