index.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. import { computed, defineComponent, nextTick, ref } from 'vue'
  2. import styles from './index.module.less'
  3. import icon1 from '../images/icon1.png'
  4. import iconArrow from '../images/icon-arrow.png'
  5. import iconArrow1 from '../images/icon-arrow1.png'
  6. import iconArrow11 from '../images/icon-arrow1-1.png'
  7. import { Popover } from 'vant'
  8. import * as echarts from 'echarts/core'
  9. import {
  10. LineChart
  11. // LineSeriesOption
  12. } from 'echarts/charts'
  13. // import { PieChart } from 'echarts/charts'
  14. import {
  15. TitleComponent,
  16. // 组件类型的定义后缀都为 ComponentOption
  17. // TitleComponentOption,
  18. TooltipComponent,
  19. // TooltipComponentOption,
  20. GridComponent,
  21. // 数据集组件
  22. DatasetComponent,
  23. // DatasetComponentOption,
  24. // 内置数据转换器组件 (filter, sort)
  25. // TransformComponent,
  26. LegendComponent,
  27. ToolboxComponent,
  28. DataZoomComponent
  29. } from 'echarts/components'
  30. import { LabelLayout } from 'echarts/features'
  31. import { CanvasRenderer } from 'echarts/renderers'
  32. import request from '@/helpers/request'
  33. import dayjs from 'dayjs'
  34. import { listenerMessage, postMessage } from '@/helpers/native-message'
  35. import { browser } from '@/helpers/utils'
  36. import { useRouter } from 'vue-router'
  37. // 注册必须的组件
  38. echarts.use([
  39. TitleComponent,
  40. TooltipComponent,
  41. GridComponent,
  42. DatasetComponent,
  43. // TransformComponent,
  44. LabelLayout,
  45. // UniversalTransition,
  46. CanvasRenderer,
  47. // PieChart,
  48. ToolboxComponent,
  49. LegendComponent,
  50. DataZoomComponent,
  51. LineChart
  52. ])
  53. const lineChartOption = (
  54. xAxisData: any,
  55. seriesData: any,
  56. countMaxCount = 5
  57. ) => {
  58. return {
  59. // title: {
  60. // text: '单位:次',
  61. // textStyle: {
  62. // color: '#777777',
  63. // fontSize: 13,
  64. // fontWeight: 400
  65. // }
  66. // },
  67. legend: { show: false },
  68. emphasis: { lineStyle: { width: 2 } },
  69. xAxis: {
  70. data: xAxisData,
  71. type: 'category',
  72. axisLine: { lineStyle: { color: '#8C8C8C' } },
  73. lineStyle: { color: '#F2F2F2' },
  74. boundaryGap: true,
  75. axisLabel: {
  76. // formatter: function (value, index) {
  77. // // 第一个和最后一个标签分别居左和居右显示
  78. // if (index === 0) {
  79. // return '{left|' + value + '}'
  80. // } else if (index === xAxisData.length - 1) {
  81. // return '{right|' + value + '}'
  82. // } else {
  83. // return value
  84. // }
  85. // },
  86. // rich: {
  87. // left: {
  88. // align: 'left'
  89. // },
  90. // right: {
  91. // align: 'right'
  92. // }
  93. // }
  94. // align: 'left'
  95. }
  96. },
  97. color: [
  98. '#2DC7AA',
  99. '#FF6079'
  100. // '#2DC7AA',
  101. // '#FF602C',
  102. // '#91DD1C',
  103. // '#FFA92C',
  104. // '#BE7E2E',
  105. // '#1C96DD',
  106. // '#D22CFF',
  107. // '#FF3C3C',
  108. // '#1AEE3E',
  109. // '#00c9ff'
  110. ],
  111. series: [
  112. {
  113. lineStyle: { width: 2 },
  114. data: seriesData[0],
  115. symbol: 'circle',
  116. showSymbol: false,
  117. name: '浏览次数',
  118. type: 'line',
  119. emphasis: { lineStyle: { width: 2 } }
  120. },
  121. {
  122. lineStyle: { width: 2 },
  123. data: seriesData[1],
  124. symbol: 'circle',
  125. showSymbol: false,
  126. name: '购买次数',
  127. type: 'line',
  128. areaStyle: {
  129. color: {
  130. type: 'linear',
  131. x: 0,
  132. y: 0,
  133. x2: 0,
  134. y2: 1,
  135. colorStops: [
  136. {
  137. offset: 0,
  138. color: 'rgba(255, 96, 121, 0.23)'
  139. // 0% 处的颜色
  140. },
  141. {
  142. offset: 1,
  143. // 100% 处的颜色
  144. color: 'rgba(255, 96, 121, 0)'
  145. }
  146. ]
  147. }
  148. },
  149. emphasis: { lineStyle: { width: 2 } }
  150. }
  151. ],
  152. grid: {
  153. bottom: '3%',
  154. containLabel: true,
  155. left: '4%',
  156. right: '4%',
  157. top: '20'
  158. },
  159. tooltip: {
  160. trigger: 'axis',
  161. position: function (point) {
  162. // 固定在顶部
  163. return [point[0], '10%']
  164. },
  165. confine: true,
  166. formatter: function (params: any) {
  167. return params[0].name
  168. },
  169. backgroundColor: '#FF6079',
  170. borderWidth: 0,
  171. borderRadius: 24,
  172. padding: [1, 4],
  173. textStyle: {
  174. color: '#FFFFFF',
  175. fontSize: 12
  176. },
  177. extraCssText: 'z-index: 2',
  178. axisPointer: {
  179. lineStyle: {
  180. color: '#FF6079'
  181. }
  182. }
  183. },
  184. yAxis: {
  185. type: 'value',
  186. splitLine: {
  187. axisLine: { lineStyle: { color: '#F2F2F2' } },
  188. lineStyle: { color: ['#f2f2f2'], type: 'dashed' }
  189. },
  190. splitNumber: countMaxCount
  191. },
  192. dataZoom: [{ type: 'inside', throttle: 100 }]
  193. // toolbox: { feature: { saveAsImage: { show: false } } }
  194. }
  195. }
  196. export type TIME_TYPE = 'MONTH' | 'THREE_MONTH' | 'HALF_YEAR' | 'YEAR'
  197. /** 获取时间范围 */
  198. export const getTimeRange = (type: TIME_TYPE) => {
  199. if (type === 'MONTH') {
  200. return {
  201. startTime: dayjs().format('YYYY-MM') + '-01',
  202. endTime: dayjs().format('YYYY-MM-DD')
  203. }
  204. } else if (type === 'THREE_MONTH') {
  205. return {
  206. startTime: dayjs().subtract(3, 'month').format('YYYY-MM-DD'),
  207. endTime: dayjs().format('YYYY-MM-DD')
  208. }
  209. } else if (type === 'HALF_YEAR') {
  210. return {
  211. startTime: dayjs().subtract(6, 'month').format('YYYY-MM-DD'),
  212. endTime: dayjs().format('YYYY-MM-DD')
  213. }
  214. } else if (type === 'YEAR') {
  215. return {
  216. startTime: dayjs().subtract(1, 'year').format('YYYY-MM-DD'),
  217. endTime: dayjs().format('YYYY-MM-DD')
  218. }
  219. }
  220. }
  221. export default defineComponent({
  222. name: 'HomeStatistics',
  223. setup() {
  224. const homeStatisticsRef = ref()
  225. const router = useRouter()
  226. const popoverStatus = ref(false)
  227. const currentType = ref<TIME_TYPE>('MONTH')
  228. const timeRange = ref(getTimeRange(currentType.value))
  229. const statisticCounts = ref({
  230. browseCount: 0,
  231. buyCount: 0,
  232. time: ''
  233. })
  234. let myChart: echarts.ECharts
  235. nextTick(() => {
  236. myChart = echarts.init(
  237. document.getElementById('eChart') as HTMLDivElement
  238. )
  239. getDetail()
  240. const round = homeStatisticsRef.value?.getBoundingClientRect()
  241. postMessage({
  242. api: 'homeStatisticsHeight',
  243. content: {
  244. height: round.height || 300
  245. }
  246. })
  247. })
  248. const searchText = computed(() => {
  249. const template = {
  250. MONTH: '本月',
  251. THREE_MONTH: '近三个月',
  252. HALF_YEAR: '近半年',
  253. YEAR: '近一年'
  254. }
  255. return template[currentType.value]
  256. })
  257. const getDetail = async () => {
  258. try {
  259. const { data } = await request.post(
  260. '/api-teacher/home/courseExposure',
  261. {
  262. data: timeRange.value
  263. }
  264. )
  265. const buy = data.buy || []
  266. const exposure = data.exposure || []
  267. const xAxisData: string[] = []
  268. const exposureList: number[] = []
  269. let maxCount = 0 // 最大人数 - 用记设置练习人数分割线
  270. exposure.forEach((item: any, index: number) => {
  271. xAxisData.push(item.date)
  272. exposureList.push(item.exposureNum)
  273. if (maxCount < (item.exposureNum || 0)) {
  274. maxCount = item.exposureNum
  275. }
  276. if (exposure.length - 1 === index) {
  277. statisticCounts.value.browseCount = item.exposureNum
  278. statisticCounts.value.time = item.date
  279. }
  280. })
  281. const buyList: number[] = []
  282. buy.forEach((item: any, index: number) => {
  283. buyList.push(item.exposureNum)
  284. if (buy.length - 1 === index) {
  285. statisticCounts.value.buyCount = item.exposureNum
  286. }
  287. })
  288. const yAxisData = [exposureList, buyList]
  289. const countMaxCount = maxCount >= 5 ? 5 : Math.max(maxCount, 1)
  290. myChart.clear()
  291. lineChartOption &&
  292. myChart.setOption(
  293. lineChartOption(xAxisData, yAxisData, countMaxCount),
  294. true
  295. )
  296. myChart.on('highlight', function (params: any) {
  297. const batch = params.batch || []
  298. const options: any = myChart.getOption()
  299. batch.forEach((item: any) => {
  300. const batchIndex = item.dataIndex
  301. const browseCount = options.series[0].data[batchIndex]
  302. const buyCount = options.series[1].data[batchIndex]
  303. const time = options.xAxis[0].data[batchIndex]
  304. statisticCounts.value = {
  305. browseCount,
  306. buyCount,
  307. time
  308. }
  309. })
  310. })
  311. // 可能出现多个时,图表同时渲染,提示有问题
  312. setTimeout(() => {
  313. const lastIndex = yAxisData[0].length - 1
  314. myChart.dispatchAction({
  315. type: 'showTip',
  316. seriesIndex: 0, // 系列索引
  317. dataIndex: lastIndex // 数据索引
  318. })
  319. }, 0)
  320. } catch {
  321. //
  322. }
  323. }
  324. const onChangeTime = (type: TIME_TYPE) => {
  325. popoverStatus.value = false
  326. if (currentType.value === type) return
  327. currentType.value = type
  328. timeRange.value = getTimeRange(currentType.value)
  329. getDetail()
  330. }
  331. /** 跳转详情 */
  332. const goDetail = () => {
  333. if (browser().isApp) {
  334. postMessage({
  335. api: 'openWebView',
  336. content: {
  337. url: `${location.origin}/teacher/#/home-statistics-detail?currentType=${currentType.value}`,
  338. orientation: 1,
  339. isHideTitle: false
  340. }
  341. })
  342. } else {
  343. router.push({
  344. path: '/home-statistics-detail',
  345. query: {
  346. currentType: currentType.value
  347. }
  348. })
  349. }
  350. }
  351. // 监听页面返回
  352. listenerMessage('webViewOnResume', () => {
  353. getDetail()
  354. })
  355. return () => (
  356. <div class={styles.homeStatistics} ref={homeStatisticsRef}>
  357. <div class={styles.homeHead}>
  358. <div class={styles.title}>
  359. <img src={icon1} />
  360. <span>浏览/购买</span>
  361. </div>
  362. <div class={styles.more} onClick={goDetail}>
  363. <span>详情</span>
  364. <img src={iconArrow} />
  365. </div>
  366. </div>
  367. <div class={styles.eChartSection}>
  368. <div class={styles.eChartTitle}>
  369. <div class={styles.left}>
  370. {statisticCounts.value.time && (
  371. <div class={styles.time}>{statisticCounts.value.time}</div>
  372. )}
  373. <div class={styles.twoItem}>
  374. <div class={styles.item} style="--color: #2DC7AA">
  375. <span class={styles.line}></span>
  376. <span class={styles.text}>浏览次数</span>
  377. <span class={styles.num}>
  378. {statisticCounts.value.browseCount}次
  379. </span>
  380. </div>
  381. <div class={styles.item} style="--color: #FF6079">
  382. <span class={styles.line}></span>
  383. <span class={styles.text}>购买次数</span>
  384. <span class={styles.num}>
  385. {statisticCounts.value.buyCount}次
  386. </span>
  387. </div>
  388. </div>
  389. </div>
  390. </div>
  391. <div class={styles.unit}>
  392. <div class={styles.unitText}>单位:次</div>
  393. <div class={styles.right}>
  394. <Popover
  395. v-model:show={popoverStatus.value}
  396. showArrow={false}
  397. placement="bottom-end"
  398. >
  399. {{
  400. default: () => (
  401. <div class={'select-time'}>
  402. <span
  403. onClick={() => onChangeTime('MONTH')}
  404. class={currentType.value === 'MONTH' ? 'active' : ''}
  405. >
  406. 本月
  407. </span>
  408. <span
  409. onClick={() => onChangeTime('THREE_MONTH')}
  410. class={
  411. currentType.value === 'THREE_MONTH' ? 'active' : ''
  412. }
  413. >
  414. 近三个月
  415. </span>
  416. <span
  417. onClick={() => onChangeTime('HALF_YEAR')}
  418. class={
  419. currentType.value === 'HALF_YEAR' ? 'active' : ''
  420. }
  421. >
  422. 近半年
  423. </span>
  424. <span
  425. onClick={() => onChangeTime('YEAR')}
  426. class={currentType.value === 'YEAR' ? 'active' : ''}
  427. >
  428. 近一年
  429. </span>
  430. </div>
  431. ),
  432. reference: () => (
  433. <div
  434. class={[
  435. styles.showItem,
  436. popoverStatus.value && styles.showItemActive
  437. ]}
  438. >
  439. <span>{searchText.value}</span>
  440. <img
  441. src={popoverStatus.value ? iconArrow11 : iconArrow1}
  442. />
  443. </div>
  444. )
  445. }}
  446. </Popover>
  447. </div>
  448. </div>
  449. <div class={styles.eChart}>
  450. <div id="eChart" style="width: 100%; height: 100%;"></div>
  451. </div>
  452. </div>
  453. </div>
  454. )
  455. }
  456. })