total-detail.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596
  1. <template>
  2. <div class="total-detail-container">
  3. <!-- 标题和链接区域 -->
  4. <div class="item-header">
  5. <div class="title-section">
  6. <div class="item-title">整体</div>
  7. <div class="item-link report-link"
  8. @click="handleReportLinkClick">整体报表</div>
  9. </div>
  10. <div class="item-links">
  11. <div class="item-link"
  12. v-if="!role.includes('SecurityCheck') && !(role.includes('banzuzhang') && selectedRole == 'individual')"
  13. @click="handleLinkClick('/pages/capabilityComparison/index')">能力对比</div>
  14. </div>
  15. </div>
  16. <!-- 雷达图区域 -->
  17. <div class="chart-section">
  18. <div class="chart-container">
  19. <div id="radar-chart" class="radar-chart"></div>
  20. </div>
  21. </div>
  22. <!-- 数据展示区域 -->
  23. <div class="data-section">
  24. <div v-for="(item, index) in dataItems" :key="index" class="data-item">
  25. <div class="item-title">{{ item.title }}</div>
  26. <div class="data-content-section">
  27. <div class="data-content" v-for="ele in item.list">
  28. <div class="data-value">{{ ele.value }}</div>
  29. <div class="data-label">{{ ele.label }}</div>
  30. </div>
  31. </div>
  32. </div>
  33. </div>
  34. </div>
  35. </template>
  36. <script>
  37. import * as echarts from 'echarts';
  38. export default {
  39. name: 'TotalDetail',
  40. data() {
  41. return {
  42. chart: null,
  43. dataItems: []
  44. }
  45. },
  46. props: {
  47. homePageWholeData: {
  48. type: Array,
  49. default: () => []
  50. },
  51. startDate: {
  52. type: String,
  53. default: ''
  54. },
  55. endDate: {
  56. type: String,
  57. default: ''
  58. },
  59. timeRange: {
  60. type: String,
  61. default: 'year'
  62. },
  63. selectedRole: {
  64. type: String,
  65. default: ''
  66. }
  67. },
  68. watch: {
  69. homePageWholeData: {
  70. handler(newValue) {
  71. this.updateData(newValue);
  72. },
  73. deep: true,
  74. immediated: true,
  75. }
  76. },
  77. computed: {
  78. role() {
  79. return this.$store?.state?.user?.roles
  80. },
  81. currentUser() {
  82. return this.$store.state.user;
  83. }
  84. },
  85. mounted() {
  86. // this.initChart();
  87. },
  88. beforeDestroy() {
  89. if (this.chart) {
  90. this.chart.dispose();
  91. }
  92. },
  93. methods: {
  94. updateData(newValue) {
  95. console.log(newValue, "newValue")
  96. this.dataItems = newValue.map(item => {
  97. return {
  98. title: item.name + "数据明细",
  99. list: [
  100. { label: '人均查获数量', value: item.seizureCount || 0 },
  101. { label: '人均在岗时长', value: item.workingHours || 0 },
  102. { label: '巡视检查合格率', value: `${((item.checkPassRate * 100) || 0).toFixed(2)}%` },
  103. { label: '抽问抽答正确率', value: `${((item.answersAccuracy * 100) || 0).toFixed(2)}%` },
  104. { label: '培训答题平均分', value: item.learningGrowthScore || 0 }
  105. ]
  106. }
  107. })
  108. // 同时更新雷达图数据
  109. this.updateChartWithData();
  110. },
  111. handleLinkClick(link) {
  112. if (link) {
  113. const roles = this.currentUser.roles;
  114. const userInfo = this.currentUser.userInfo;
  115. let obj = {};
  116. if (roles.includes('SecurityCheck')) {
  117. obj = {
  118. id: userInfo.userId,
  119. name: userInfo.nickName,
  120. type: 'USER'
  121. }
  122. }
  123. if (roles.includes('banzuzhang')) {
  124. obj = {
  125. id: userInfo.teamsId,
  126. name: userInfo.teamsName,
  127. type: 'DEPT'
  128. }
  129. }
  130. if (roles.includes('kezhang')) {
  131. obj = {
  132. id: userInfo.departmentId,
  133. name: userInfo.departmentName,
  134. type: 'DEPT'
  135. }
  136. }
  137. if (roles.includes('test') || roles.includes('zhijianke')) {
  138. obj = {
  139. id: userInfo.stationId,
  140. name: userInfo.stationName,
  141. type: 'DEPT'
  142. }
  143. }
  144. // 添加时间参数
  145. const timeParams = {
  146. startDate: this.startDate,
  147. endDate: this.endDate,
  148. timeRange: this.timeRange
  149. };
  150. // 合并所有参数
  151. const allParams = {
  152. ...obj,
  153. ...timeParams
  154. };
  155. // 构建带参数的URL
  156. let finalUrl = link;
  157. if (Object.keys(allParams).length > 0) {
  158. // 检查URL是否已有参数
  159. const separator = link.includes('?') ? '&' : '?';
  160. const queryString = Object.keys(allParams)
  161. .filter(key => allParams[key]) // 过滤掉空值
  162. .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(allParams[key])}`)
  163. .join('&');
  164. if (queryString) {
  165. finalUrl = `${link}${separator}${queryString}`;
  166. }
  167. }
  168. uni.navigateTo({
  169. url: finalUrl
  170. });
  171. }
  172. },
  173. updateChartWithData() {
  174. const chartDom = document.getElementById('radar-chart');
  175. if (!chartDom) {
  176. console.warn('雷达图容器未找到');
  177. return;
  178. }
  179. this.chart = echarts.init(chartDom);
  180. // 根据接口数据更新雷达图
  181. if (this.chart && this.homePageWholeData && this.homePageWholeData.length > 0) {
  182. // 使用Graph后缀的数据来显示雷达图
  183. const seriesData = this.homePageWholeData.map(item => ({
  184. name: item.name,
  185. value: [
  186. item.seizureCountGraph || 0,
  187. item.workingHoursGraph || 0,
  188. item.checkPassRateGraph || 0,
  189. item.answersAccuracyGraph || 0,
  190. item.learningGrowthScoreGraph || 0
  191. ]
  192. }));
  193. // 计算每个指标的最大值,用于雷达图的max值
  194. const indicators = [
  195. { name: '查获能力', max: Math.max(...seriesData.map(d => d.value[0])) },
  196. { name: '在岗时长', max: Math.max(...seriesData.map(d => d.value[1])) },
  197. { name: '巡视检查合格率', max: Math.max(...seriesData.map(d => d.value[2])) },
  198. { name: '抽问抽答', max: Math.max(...seriesData.map(d => d.value[3])) },
  199. { name: '培训答题', max: Math.max(...seriesData.map(d => d.value[4])) }
  200. ];
  201. const option = {
  202. legend: {
  203. type: 'scroll',
  204. orient: 'horizontal',
  205. bottom: 10,
  206. textStyle: {
  207. fontSize: 12,
  208. color: '#666'
  209. },
  210. itemWidth: 12,
  211. itemHeight: 12,
  212. data: seriesData.map(item => item.name)
  213. },
  214. radar: {
  215. shape: 'circle',
  216. indicator: indicators,
  217. splitNumber: 4,
  218. center: ['50%', '45%'],
  219. radius: '55%',
  220. axisName: {
  221. color: '#666',
  222. fontSize: 12
  223. },
  224. splitLine: {
  225. lineStyle: {
  226. color: ['#E6E6E6', '#E6E6E6', '#E6E6E6', '#E6E6E6']
  227. }
  228. },
  229. splitArea: {
  230. show: true,
  231. areaStyle: {
  232. color: ['#F8F8F8', '#FFFFFF']
  233. }
  234. },
  235. axisLine: {
  236. lineStyle: {
  237. color: '#E6E6E6'
  238. }
  239. }
  240. },
  241. series: [
  242. {
  243. name: '能力对比',
  244. type: 'radar',
  245. data: seriesData.map((item, index) => ({
  246. value: item.value,
  247. name: item.name,
  248. areaStyle: {
  249. color: this.getChartColor(index, 0.3)
  250. },
  251. lineStyle: {
  252. color: this.getChartColor(index),
  253. width: 2
  254. },
  255. itemStyle: {
  256. color: this.getChartColor(index)
  257. }
  258. }))
  259. }
  260. ],
  261. // 添加点击事件
  262. tooltip: {
  263. show: false
  264. },
  265. };
  266. this.chart.setOption(option);
  267. // 响应式调整
  268. window.addEventListener('resize', () => {
  269. this.chart.resize();
  270. });
  271. }
  272. },
  273. // 获取图表颜色
  274. getChartColor(index, opacity = 1) {
  275. const colors = ['#8FA5EC', '#FF9F7F', '#6ECEB2', '#FFD700', '#BA55D3'];
  276. const color = colors[index % colors.length];
  277. if (opacity < 1) {
  278. // 转换为RGBA格式
  279. const r = parseInt(color.slice(1, 3), 16);
  280. const g = parseInt(color.slice(3, 5), 16);
  281. const b = parseInt(color.slice(5, 7), 16);
  282. return `rgba(${r}, ${g}, ${b}, ${opacity})`;
  283. }
  284. return color;
  285. },
  286. // 处理雷达图点击事件
  287. handleRadarChartClick(params) {
  288. if (params.componentType === 'series' && params.seriesType === 'radar') {
  289. const clickedData = params.data;
  290. const seriesIndex = params.seriesIndex;
  291. const dataIndex = params.dataIndex;
  292. // 获取对应的数据项
  293. const dataItem = this.homePageWholeData[dataIndex];
  294. if (dataItem) {
  295. // 显示详细数据弹窗
  296. this.showRadarDataDetail(dataItem);
  297. }
  298. }
  299. },
  300. // 显示雷达图数据详情
  301. showRadarDataDetail(dataItem) {
  302. const labels = ['查获数量', '在岗时长', '巡视检查合格率', '抽问抽答', '培训答题'];
  303. const values = [
  304. dataItem.seizureCount || 0,
  305. dataItem.workingHours || 0,
  306. dataItem.checkPassRate || 0,
  307. dataItem.answersAccuracy || 0,
  308. dataItem.learningGrowthScore || 0
  309. ];
  310. let detailHtml = `<div style="font-size: 16px; font-weight: bold; margin-bottom: 12px; text-align: center;">${dataItem.name} - 详细数据</div>`;
  311. labels.forEach((label, index) => {
  312. detailHtml += `<div style="display: flex; justify-content: space-between; margin: 8px 0; padding: 4px 0; border-bottom: 1px solid #f0f0f0;">
  313. <span style="color: #666;">${label}:</span>
  314. <span style="font-weight: bold; color: #1890ff;">${values[index]}</span>
  315. </div>`;
  316. });
  317. uni.showModal({
  318. title: '详细数据',
  319. content: detailHtml,
  320. showCancel: false,
  321. confirmText: '关闭',
  322. confirmColor: '#1890ff'
  323. });
  324. },
  325. // 处理整体报表链接点击
  326. handleReportLinkClick() {
  327. // 根据时间范围自动计算开始和结束时间
  328. const { startDate, endDate } = this.calculateDateRange(this.timeRange);
  329. // 构建跳转参数
  330. const params = {
  331. startDate: startDate,
  332. endDate: endDate,
  333. };
  334. // 过滤掉空值
  335. const queryParams = Object.keys(params)
  336. .filter(key => params[key] && params[key] !== '')
  337. .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
  338. .join('&');
  339. // 构建跳转URL
  340. const url = queryParams ?
  341. `/pages/statisticalReport/index?${queryParams}` :
  342. '/pages/statisticalReport/index';
  343. uni.navigateTo({
  344. url: url
  345. });
  346. },
  347. // 根据时间范围计算开始和结束时间
  348. calculateDateRange(timeRange) {
  349. const today = new Date();
  350. const yesterday = new Date(today);
  351. yesterday.setDate(today.getDate() - 1);
  352. let startDate = new Date(yesterday);
  353. let endDate = new Date(yesterday);
  354. switch (timeRange) {
  355. case 'week':
  356. // 近一周:从7天前到昨天
  357. startDate.setDate(yesterday.getDate() - 6);
  358. break;
  359. case 'month':
  360. // 近一月:从30天前到昨天
  361. startDate.setDate(yesterday.getDate() - 29);
  362. break;
  363. case 'quarter':
  364. // 近三月:从90天前到昨天
  365. startDate.setDate(yesterday.getDate() - 89);
  366. break;
  367. case 'halfYear':
  368. // 近半年:从180天前到昨天
  369. startDate.setDate(yesterday.getDate() - 179);
  370. break;
  371. case 'year':
  372. // 近一年:从365天前到昨天
  373. startDate.setDate(yesterday.getDate() - 364);
  374. break;
  375. case 'custom':
  376. // 自定义时间:使用用户选择的日期
  377. if (this.startDate && this.endDate) {
  378. startDate = new Date(this.startDate);
  379. endDate = new Date(this.endDate);
  380. }
  381. break;
  382. default:
  383. // 默认近一年
  384. startDate.setDate(yesterday.getDate() - 364);
  385. }
  386. return {
  387. startDate: this.formatDateForInput(startDate),
  388. endDate: this.formatDateForInput(endDate)
  389. };
  390. },
  391. // 格式化日期为输入框格式
  392. formatDateForInput(date) {
  393. const year = date.getFullYear();
  394. const month = String(date.getMonth() + 1).padStart(2, '0');
  395. const day = String(date.getDate()).padStart(2, '0');
  396. return `${year}-${month}-${day}`;
  397. }
  398. }
  399. }
  400. </script>
  401. <style lang="scss" scoped>
  402. .total-detail-container {
  403. background: #FFFFFF;
  404. border-radius: 20rpx;
  405. padding: 30rpx;
  406. margin-bottom: 30rpx;
  407. box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
  408. }
  409. .item-header {
  410. display: flex;
  411. justify-content: space-between;
  412. align-items: flex-start;
  413. margin-bottom: 25rpx;
  414. }
  415. .title-section {
  416. display: flex;
  417. align-items: center;
  418. gap: 20rpx;
  419. }
  420. .item-title {
  421. font-size: 32rpx;
  422. font-weight: bold;
  423. color: #333333;
  424. margin-bottom: 0;
  425. }
  426. .item-link {
  427. font-size: 26rpx;
  428. color: #1890FF;
  429. cursor: pointer;
  430. }
  431. .report-link {
  432. margin-left: 0;
  433. }
  434. .chart-section {
  435. margin-bottom: 30rpx;
  436. }
  437. .chart-container {
  438. width: 100%;
  439. height: 600rpx;
  440. margin-bottom: 20rpx;
  441. }
  442. .radar-chart {
  443. width: 100%;
  444. height: 100%;
  445. }
  446. .chart-legend {
  447. display: flex;
  448. justify-content: center;
  449. gap: 40rpx;
  450. }
  451. .legend-item {
  452. display: flex;
  453. align-items: center;
  454. gap: 10rpx;
  455. }
  456. .legend-color {
  457. width: 20rpx;
  458. height: 20rpx;
  459. border-radius: 4rpx;
  460. }
  461. .legend-text {
  462. font-size: 24rpx;
  463. color: #666;
  464. }
  465. .data-section {}
  466. .data-item {
  467. background: #EFF2FE;
  468. border-radius: 15rpx;
  469. padding: 20rpx;
  470. text-align: center;
  471. display: flex;
  472. flex-direction: column;
  473. align-items: flex-start;
  474. margin-bottom: 30rpx;
  475. }
  476. .data-content-section {
  477. display: flex;
  478. }
  479. .data-content {
  480. flex: 1;
  481. display: flex;
  482. flex-direction: column;
  483. align-items: center;
  484. }
  485. .data-value {
  486. font-size: 32rpx;
  487. font-weight: bold;
  488. color: #333333;
  489. margin-bottom: 8rpx;
  490. }
  491. .data-label {
  492. font-size: 24rpx;
  493. color: #666;
  494. }
  495. .rank-section {
  496. margin-top: 30rpx;
  497. }
  498. .rank-item {
  499. display: flex;
  500. align-items: center;
  501. margin-bottom: 10rpx;
  502. }
  503. .rank-label {
  504. width: 80rpx;
  505. font-size: 26rpx;
  506. color: #333;
  507. font-weight: 500;
  508. }
  509. .rank-progress {
  510. flex: 1;
  511. margin: 0 20rpx;
  512. }
  513. .rank-info {
  514. width: 100rpx;
  515. text-align: right;
  516. font-size: 26rpx;
  517. color: #666;
  518. }
  519. </style>