index.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. import { computed, defineComponent, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
  2. import styles from './index.module.less'
  3. import CircleProgress from './component/CircleProgress'
  4. import iconBackup from './image/icon-backup.png'
  5. import iconEnsemble from './image/icon-ensemble.png'
  6. import * as echarts from 'echarts'
  7. import { listenerMessage, postMessage, removeListenerMessage } from '@/helpers/native-message'
  8. type EChartsOption = echarts.EChartsOption
  9. interface ISubjectItem {
  10. /** 声部名称 */
  11. subjectName: string
  12. /**达标率 */
  13. practiceRate: number
  14. /** 达标人数 */
  15. passNum: number
  16. /** 未达标人数 */
  17. noPassNum: number
  18. /** 非会员 */
  19. noMemberNum: number
  20. }
  21. const symbolData: any = {
  22. type: 'line',
  23. symbol: 'circle',
  24. symbolSize: 6,
  25. triggerLineEvent: true
  26. }
  27. export default defineComponent({
  28. name: 'subject-echarts',
  29. setup() {
  30. const activeData = reactive({
  31. index: -1,
  32. sum: {
  33. /**达标率 */
  34. practiceRate: 0,
  35. /**达标人数 */
  36. passNum: 0,
  37. /**未达标人数 */
  38. noPassNum: 0,
  39. /**未会员 */
  40. noMemberNum: 0
  41. },
  42. practiceThisWeeks: [] as ISubjectItem[],
  43. colors: [
  44. {
  45. color: '#FF8057',
  46. borderColor: 'rgba(255,128,87,0.5)',
  47. text: '达标率',
  48. select: true,
  49. key: 'practiceRate'
  50. },
  51. {
  52. color: '#2FC58D',
  53. borderColor: 'rgba(47,197,141,0.5)',
  54. text: '达标',
  55. select: true,
  56. key: 'passNum'
  57. },
  58. {
  59. color: '#4A99FF',
  60. borderColor: 'rgba(74,153,255,0.5)',
  61. text: '未达标',
  62. select: true,
  63. key: 'noPassNum'
  64. },
  65. {
  66. color: '#9884BA',
  67. borderColor: 'rgba(152,132,186,0.5)',
  68. text: '非会员',
  69. select: true,
  70. key: 'noMemberNum'
  71. }
  72. ]
  73. })
  74. const subjects = computed(() => {
  75. return activeData.practiceThisWeeks.map((n) => n.subjectName)
  76. })
  77. let myChart: echarts.ECharts
  78. const handleInit = (data: any) => {
  79. const { content } = data
  80. if (!content) return
  81. if (myChart) {
  82. myChart.dispose()
  83. }
  84. activeData.sum = content.sum || {}
  85. activeData.practiceThisWeeks = content.practiceThisWeeks || []
  86. const chartDom = document.getElementById('subjectEcharts')!
  87. myChart = echarts.init(chartDom)
  88. const option: EChartsOption = {
  89. tooltip: {
  90. trigger: 'axis',
  91. showContent: false,
  92. axisPointer: {
  93. type: 'line',
  94. lineStyle: {
  95. width: 30,
  96. type: 'solid',
  97. opacity: 0.2
  98. }
  99. }
  100. },
  101. grid: {
  102. left: 8,
  103. top: 5,
  104. right: 5,
  105. bottom: 5,
  106. containLabel: true
  107. },
  108. legend: {
  109. type: 'plain',
  110. align: 'auto',
  111. itemGap: 10,
  112. itemWidth: 6,
  113. itemStyle: {
  114. opacity: 0
  115. },
  116. lineStyle: {
  117. width: 0,
  118. opacity: 0
  119. },
  120. textStyle: {
  121. opacity: 0
  122. }
  123. },
  124. xAxis: {
  125. type: 'category',
  126. axisLine: {
  127. show: false
  128. },
  129. axisLabel: {
  130. interval: 0,
  131. color: '#333',
  132. fontSize: 9
  133. },
  134. axisTick: {
  135. alignWithLabel: true,
  136. show: false
  137. },
  138. // boundaryGap: false,
  139. data: subjects.value
  140. },
  141. yAxis: [
  142. {
  143. type: 'value',
  144. position: 'left',
  145. axisLine: {
  146. show: false,
  147. lineStyle: {
  148. color: '#999'
  149. }
  150. },
  151. splitLine: {
  152. show: true, // 是否显示y轴分割线
  153. lineStyle: {
  154. color: ['rgba(249, 234, 220, 1)'] // 分隔线颜色。
  155. }
  156. },
  157. min: 0,
  158. splitNumber: 5
  159. },
  160. {
  161. type: 'value',
  162. position: 'right',
  163. splitLine: {
  164. show: true, // 是否显示y轴分割线
  165. lineStyle: {
  166. color: ['rgba(249, 234, 220, 1)'] // 分隔线颜色。
  167. }
  168. },
  169. axisLine: {
  170. show: false,
  171. lineStyle: {
  172. color: '#999'
  173. }
  174. },
  175. interval: 20,
  176. min: 0,
  177. max: 100,
  178. splitNumber: 5,
  179. axisLabel: {
  180. formatter: function (value: number, index: number) {
  181. return value + '%'
  182. }
  183. }
  184. }
  185. ],
  186. series: ['practiceRate', 'passNum', 'noPassNum', 'noMemberNum'].map(
  187. (key: string, index: number) => {
  188. return {
  189. name: activeData.colors[index].text,
  190. color: activeData.colors[index].color,
  191. ...symbolData,
  192. yAxisIndex: index === 0 ? 1 : 0,
  193. data: activeData.practiceThisWeeks.map((n) => n[key])
  194. }
  195. }
  196. )
  197. }
  198. let maxNum = Math.max(...(option.series as any[]).map((n) => n.data).flat())
  199. let minNum = Math.min(...(option.series as any[]).map((n) => n.data).flat())
  200. maxNum = Math.ceil(maxNum / 9.5) * 10
  201. minNum = Math.floor(minNum / 12) * 10
  202. ;(option.yAxis as any[])[0].interval = Math.ceil(Math.ceil(maxNum) / 5)
  203. ;(option.yAxis as any[])[0].max = Math.ceil(Math.ceil(maxNum) / 5) * 5
  204. option && myChart.setOption(option)
  205. myChart.on('highlight', function (params: any) {
  206. activeData.index = params.batch[0].dataIndex
  207. })
  208. }
  209. const handleAction = (index: number) => {
  210. activeData.index = index
  211. myChart?.dispatchAction({
  212. type: 'showTip',
  213. seriesIndex: 0,
  214. dataIndex: index
  215. })
  216. }
  217. const changeLegend = (item: any) => {
  218. myChart?.dispatchAction({
  219. type: item.select ? 'legendUnSelect' : 'legendSelect',
  220. // 图例名称
  221. name: item.text
  222. })
  223. item.select = !item.select
  224. }
  225. const handleMock = () => {
  226. // console.log('handleMock1232')
  227. const resulst = {
  228. sum: {
  229. noMemberNum: Math.floor(Math.random() * 1000),
  230. noPassNum: Math.floor(Math.random() * 1000),
  231. passNum: Math.floor(Math.random() * 1000),
  232. practiceRate: Math.ceil(Math.random() * 100)
  233. },
  234. practiceThisWeeks: new Array(Math.ceil(Math.random() * 9)).fill(1).map((n, i) => {
  235. return {
  236. /** 声部名称 */
  237. subjectName: '声部' + (i + 1),
  238. /**达标率 */
  239. practiceRate: Math.ceil(Math.random() * 100),
  240. /** 达标人数 */
  241. passNum: Math.floor(Math.random() * 1000),
  242. /** 未达标人数 */
  243. noPassNum: Math.floor(Math.random() * 1000),
  244. /** 非会员 */
  245. noMemberNum: Math.floor(Math.random() * 1000)
  246. }
  247. })
  248. }
  249. console.log(resulst)
  250. handleInit({ content: resulst })
  251. }
  252. onMounted(() => {
  253. // handleMock()
  254. listenerMessage('setAccomanyEcharts', handleInit)
  255. postMessage({
  256. api: 'setAccomanyEcharts'
  257. })
  258. })
  259. onBeforeUnmount(() => {
  260. removeListenerMessage('setAccomanyEcharts', handleInit)
  261. })
  262. const goto = () => {
  263. const url = location.origin + location.pathname + `#/exercise-record`
  264. console.log('🚀 ~ url:', url)
  265. postMessage({
  266. api: 'openWebView',
  267. content: {
  268. url: url,
  269. orientation: 1
  270. }
  271. })
  272. }
  273. return () => (
  274. <div class={styles.subjectEcharts}>
  275. <div class={[styles.container, styles.ensemble]}>
  276. <div class={styles.head}>
  277. <div class={styles.headLeft}>
  278. <img class={styles.icon} src={iconEnsemble} />
  279. <div>总体情况</div>
  280. </div>
  281. </div>
  282. <div class={styles.content} onClick={() => goto()}>
  283. <CircleProgress
  284. value={Number(activeData.sum.practiceRate)}
  285. size={80}
  286. color="#FF8057"
  287. strokeWidth={6}
  288. />
  289. <div class={styles.items}>
  290. <div class={styles.item}>
  291. <div class={styles.itemNum}>
  292. <span class={styles.rect}></span>
  293. <span style={{ color: '#FF8057' }}>{activeData.sum.passNum}</span>
  294. </div>
  295. <div class={styles.itemTitle}>达标人数</div>
  296. </div>
  297. <div class={[styles.item, styles.line]}>
  298. <div class={styles.itemNum}>
  299. <span class={styles.rect} style={{ background: '#FFE7DF' }}></span>
  300. <span>{activeData.sum.noPassNum}</span>
  301. </div>
  302. <div class={styles.itemTitle}>未达标人数</div>
  303. </div>
  304. <div class={styles.item}>
  305. <div class={styles.itemNum}>
  306. <span>{activeData.sum.noMemberNum}</span>
  307. </div>
  308. <div class={styles.itemTitle}>非会员人数</div>
  309. </div>
  310. </div>
  311. </div>
  312. </div>
  313. <div class={[styles.container, styles.ensemble]}>
  314. <div class={styles.head}>
  315. <div class={styles.headLeft}>
  316. <img class={styles.icon} src={iconBackup} />
  317. <div>声部情况</div>
  318. </div>
  319. <div class={styles.headRight}>
  320. {activeData.colors.map((c) => (
  321. <div
  322. style={
  323. c.select
  324. ? { color: '#fff', borderColor: c.color, backgroundColor: c.color }
  325. : { color: c.color, borderColor: c.borderColor }
  326. }
  327. onClick={() => changeLegend(c)}
  328. >
  329. {c.text}
  330. </div>
  331. ))}
  332. </div>
  333. </div>
  334. <div class={styles.headLabelWrap}>
  335. {activeData.colors.map((c, cIndex) => {
  336. return (
  337. <div class={styles.headLabel} style={{ display: c.select ? '' : 'none' }}>
  338. <div
  339. class={styles.headLabelDot}
  340. style={{ background: c.color, color: c.color }}
  341. ></div>
  342. <div style={{ color: c.color }}>
  343. {activeData.sum[c.key] || 0}
  344. {cIndex === 0 ? '%' : '人'}
  345. </div>
  346. </div>
  347. )
  348. })}
  349. </div>
  350. <div id="subjectEcharts" class={styles.echartsMain}></div>
  351. {!activeData.practiceThisWeeks.length && <div class={styles.emtry}>暂无练习记录</div>}
  352. </div>
  353. <div class={[styles.container, styles.subjectWrap]}>
  354. {activeData.practiceThisWeeks.map((item, index: number) => (
  355. <div
  356. class={[styles.listItem, index === activeData.index ? styles.listItemActive : '']}
  357. onClick={() => handleAction(index)}
  358. >
  359. <div class={styles.dot}></div>
  360. <div class={styles.itemLeft}>
  361. <div class={styles.subjectName}>{item.subjectName}</div>
  362. <div class={styles.subjectType}>声部</div>
  363. </div>
  364. <div class={styles.listitems}>
  365. {activeData.colors.map((c, index: number) => {
  366. return (
  367. <div class={styles.item}>
  368. <div class={styles.itemNum}>
  369. <span style={{ color: c.color }}>
  370. {item[c.key]}
  371. {index === 0 ? '%' : ''}
  372. </span>
  373. </div>
  374. <div class={styles.itemTitle}>
  375. {c.text}
  376. {index === 0 ? '' : '人数'}
  377. </div>
  378. </div>
  379. )
  380. })}
  381. </div>
  382. </div>
  383. ))}
  384. </div>
  385. </div>
  386. )
  387. }
  388. })