l-echart.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. <template>
  2. <view class="lime-echart" :style="[customStyle]" v-if="canvasId" ref="limeEchart" :aria-label="ariaLabel">
  3. <!-- #ifndef APP-NVUE -->
  4. <canvas
  5. class="lime-echart__canvas"
  6. v-if="use2dCanvas"
  7. type="2d"
  8. :id="canvasId"
  9. style="width: 100%;height: 100%;"
  10. :disable-scroll="isDisableScroll"
  11. @touchstart="touchStart"
  12. @touchmove="touchMove"
  13. @touchend="touchEnd"
  14. />
  15. <canvas
  16. class="lime-echart__canvas"
  17. v-else
  18. :width="nodeWidth"
  19. :height="nodeHeight"
  20. :style="canvasStyle"
  21. :canvas-id="canvasId"
  22. :id="canvasId"
  23. :disable-scroll="isDisableScroll"
  24. @touchstart="touchStart"
  25. @touchmove="touchMove"
  26. @touchend="touchEnd"
  27. />
  28. <view class="lime-echart__mask"
  29. v-if="isPC"
  30. @mousedown="touchStart"
  31. @mousemove="touchMove"
  32. @mouseup="touchEnd"
  33. @touchstart="touchStart"
  34. @touchmove="touchMove"
  35. @touchend="touchEnd">
  36. </view>
  37. <canvas v-if="isOffscreenCanvas" :style="offscreenStyle" :canvas-id="offscreenCanvasId"></canvas>
  38. <!-- #endif -->
  39. <!-- #ifdef APP-NVUE -->
  40. <web-view
  41. class="lime-echart__canvas"
  42. :id="canvasId"
  43. :style="canvasStyle"
  44. :webview-styles="webviewStyles"
  45. ref="webview"
  46. src="/pagesStorage/components/lime-echart/static/uvue.html?v=1"
  47. @pagefinish="finished = true"
  48. @onPostMessage="onMessage"
  49. ></web-view>
  50. <!-- #endif -->
  51. </view>
  52. </template>
  53. <script>
  54. // @ts-nocheck
  55. // #ifndef APP-NVUE
  56. import {Canvas, setCanvasCreator, dispatch} from './canvas';
  57. import {wrapTouch, convertTouchesToArray, devicePixelRatio ,sleep, canIUseCanvas2d, getRect, getDeviceInfo} from './utils';
  58. // #endif
  59. // #ifdef APP-NVUE
  60. import { base64ToPath, sleep } from './utils';
  61. import {Echarts} from './nvue'
  62. // #endif
  63. const charts = {}
  64. const echartsObj = {}
  65. /**
  66. * LimeChart 图表
  67. * @description 全端兼容的eCharts
  68. * @tutorial https://ext.dcloud.net.cn/plugin?id=4899
  69. * @property {String} customStyle 自定义样式
  70. * @property {String} type 指定 canvas 类型
  71. * @value 2d 使用canvas 2d,部分小程序支持
  72. * @value '' 使用原生canvas,会有层级问题
  73. * @value bottom right 不缩放图片,只显示图片的右下边区域
  74. * @property {Boolean} isDisableScroll
  75. * @property {number} beforeDelay = [30] 延迟初始化 (毫秒)
  76. * @property {Boolean} enableHover PC端使用鼠标悬浮
  77. * @event {Function} finished 加载完成触发
  78. */
  79. export default {
  80. name: 'lime-echart',
  81. props: {
  82. // #ifdef MP-WEIXIN || MP-TOUTIAO
  83. type: {
  84. type: String,
  85. default: '2d'
  86. },
  87. // #endif
  88. // #ifdef APP-NVUE
  89. webviewStyles: Object,
  90. // hybrid: Boolean,
  91. // #endif
  92. customStyle: String,
  93. isDisableScroll: Boolean,
  94. isClickable: {
  95. type: Boolean,
  96. default: true
  97. },
  98. enableHover: Boolean,
  99. beforeDelay: {
  100. type: Number,
  101. default: 30
  102. },
  103. landscape: Boolean
  104. },
  105. data() {
  106. return {
  107. // #ifdef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
  108. use2dCanvas: true,
  109. // #endif
  110. // #ifndef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
  111. use2dCanvas: false,
  112. // #endif
  113. ariaLabel: '图表',
  114. width: null,
  115. height: null,
  116. nodeWidth: null,
  117. nodeHeight: null,
  118. // canvasNode: null,
  119. config: {},
  120. inited: false,
  121. finished: false,
  122. file: '',
  123. platform: '',
  124. isPC: false,
  125. isDown: false,
  126. isOffscreenCanvas: false,
  127. offscreenWidth: 0,
  128. offscreenHeight: 0,
  129. };
  130. },
  131. computed: {
  132. rootStyle() {
  133. if(this.landscape) {
  134. return `transform: translate(-50%,-50%) rotate(90deg); top:50%; left:50%;`
  135. }
  136. },
  137. canvasId() {
  138. return `lime-echart${this._ && this._.uid || this._uid}`
  139. },
  140. offscreenCanvasId() {
  141. return `${this.canvasId}_offscreen`
  142. },
  143. offscreenStyle() {
  144. return `width:${this.offscreenWidth}px;height: ${this.offscreenHeight}px; position: fixed; left: 99999px; background: red`
  145. },
  146. canvasStyle() {
  147. return this.rootStyle + (this.width && this.height ? ('width:' + this.width + 'px;height:' + this.height + 'px') : '')
  148. }
  149. },
  150. // #ifndef VUE3
  151. beforeDestroy() {
  152. this.clear()
  153. this.dispose()
  154. // #ifdef H5
  155. if(this.isPC) {
  156. document.removeEventListener('mousewheel', this.mousewheel)
  157. }
  158. // #endif
  159. },
  160. // #endif
  161. // #ifdef VUE3
  162. beforeUnmount() {
  163. this.clear()
  164. this.dispose()
  165. // #ifdef H5
  166. if(this.isPC) {
  167. document.removeEventListener('mousewheel', this.mousewheel)
  168. }
  169. // #endif
  170. },
  171. // #endif
  172. created() {
  173. // #ifdef H5
  174. if(!('ontouchstart' in window)) {
  175. this.isPC = true
  176. document.addEventListener('mousewheel', this.mousewheel)
  177. }
  178. // #endif
  179. // #ifdef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
  180. // const { platform } = uni.getSystemInfoSync();
  181. const { platform } = getDeviceInfo();
  182. this.isPC = /windows/i.test(platform)
  183. // #endif
  184. this.use2dCanvas = this.type === '2d' && canIUseCanvas2d()
  185. },
  186. mounted() {
  187. this.$nextTick(() => {
  188. this.$emit('finished')
  189. })
  190. },
  191. methods: {
  192. // #ifdef APP-NVUE
  193. onMessage(e) {
  194. const detail = e?.detail?.data[0] || null;
  195. const data = detail?.data
  196. const key = detail?.event
  197. const options = data?.options
  198. const event = data?.event
  199. const file = detail?.file
  200. if (key == 'log' && data) {
  201. console.log(data)
  202. }
  203. if(event) {
  204. this.chart.dispatchAction(event.replace(/"/g,''), options)
  205. }
  206. if(file) {
  207. thie.file = file
  208. }
  209. },
  210. // #endif
  211. setChart(callback) {
  212. if(!this.chart) {
  213. console.warn(`组件还未初始化,请先使用 init`)
  214. return
  215. }
  216. if(typeof callback === 'function' && this.chart) {
  217. callback(this.chart);
  218. }
  219. // #ifdef APP-NVUE
  220. if(typeof callback === 'function') {
  221. this.$refs.webview.evalJs(`setChart(${JSON.stringify(callback.toString())}, ${JSON.stringify(this.chart.options)})`);
  222. }
  223. // #endif
  224. },
  225. setOption() {
  226. if (!this.chart || !this.chart.setOption) {
  227. console.warn(`组件还未初始化,请先使用 init`)
  228. return
  229. }
  230. this.chart.setOption(...arguments);
  231. },
  232. showLoading() {
  233. if(this.chart) {
  234. this.chart.showLoading(...arguments)
  235. }
  236. },
  237. hideLoading() {
  238. if(this.chart) {
  239. this.chart.hideLoading()
  240. }
  241. },
  242. clear() {
  243. if(this.chart) {
  244. this.chart.clear()
  245. }
  246. },
  247. dispose() {
  248. if(this.chart) {
  249. this.chart.dispose()
  250. }
  251. },
  252. resize(size) {
  253. if(size && size.width && size.height) {
  254. this.height = size.height
  255. this.width = size.width
  256. if(this.chart) {this.chart.resize(size)}
  257. } else {
  258. this.$nextTick(() => {
  259. uni.createSelectorQuery()
  260. .in(this)
  261. .select(`.lime-echart`)
  262. .boundingClientRect()
  263. .exec(res => {
  264. if (res) {
  265. let { width, height } = res[0];
  266. this.width = width = width || 300;
  267. this.height = height = height || 300;
  268. this.chart.resize({width, height})
  269. }
  270. });
  271. })
  272. }
  273. },
  274. canvasToTempFilePath(args = {}) {
  275. // #ifndef APP-NVUE
  276. const { use2dCanvas, canvasId } = this;
  277. return new Promise((resolve, reject) => {
  278. const copyArgs = Object.assign({
  279. canvasId,
  280. success: resolve,
  281. fail: reject
  282. }, args);
  283. if (use2dCanvas) {
  284. delete copyArgs.canvasId;
  285. copyArgs.canvas = this.canvasNode;
  286. }
  287. uni.canvasToTempFilePath(copyArgs, this);
  288. });
  289. // #endif
  290. // #ifdef APP-NVUE
  291. this.file = ''
  292. this.$refs.webview.evalJs(`canvasToTempFilePath()`);
  293. return new Promise((resolve, reject) => {
  294. this.$watch('file', async (file) => {
  295. if(file) {
  296. const tempFilePath = await base64ToPath(file)
  297. resolve(args.success({tempFilePath}))
  298. } else {
  299. reject(args.fail({error: ``}))
  300. }
  301. })
  302. })
  303. // #endif
  304. },
  305. async init(echarts, ...args) {
  306. // #ifndef APP-NVUE
  307. if(args && args.length == 0 && !echarts) {
  308. console.error('缺少参数:init(echarts, theme?:string, opts?: object, callback?: function)')
  309. return
  310. }
  311. // #endif
  312. let theme=null,opts={},callback;
  313. Array.from(arguments).forEach(item => {
  314. if(typeof item === 'function') {
  315. callback = item
  316. }
  317. if(['string'].includes(typeof item)) {
  318. theme = item
  319. }
  320. if(typeof item === 'object') {
  321. opts = item
  322. }
  323. })
  324. if(this.beforeDelay) {
  325. await sleep(this.beforeDelay)
  326. }
  327. let config = await this.getContext();
  328. // #ifndef APP-NVUE
  329. setCanvasCreator(echarts, config)
  330. try {
  331. this.chart = echarts.init(config.canvas, theme, Object.assign({}, config, opts))
  332. if(typeof callback === 'function') {
  333. callback(this.chart)
  334. } else {
  335. return this.chart
  336. }
  337. } catch(e) {
  338. console.error(e.messges)
  339. return null
  340. }
  341. // #endif
  342. // #ifdef APP-NVUE
  343. this.chart = new Echarts(this.$refs.webview)
  344. this.$refs.webview.evalJs(`init(null, null, ${JSON.stringify(opts)}, ${theme})`)
  345. if(callback) {
  346. callback(this.chart)
  347. } else {
  348. return this.chart
  349. }
  350. // #endif
  351. },
  352. getContext() {
  353. // #ifdef APP-NVUE
  354. if(this.finished) {
  355. return Promise.resolve(this.finished)
  356. }
  357. return new Promise(resolve => {
  358. this.$watch('finished', (val) => {
  359. if(val) {
  360. resolve(this.finished)
  361. }
  362. })
  363. })
  364. // #endif
  365. // #ifndef APP-NVUE
  366. return getRect(`#${this.canvasId}`, {context: this, type: this.use2dCanvas ? 'fields': 'boundingClientRect'}).then(res => {
  367. if(res) {
  368. let dpr = devicePixelRatio
  369. let {width, height, node} = res
  370. console.log(width, height, node,'width, height, node');
  371. let canvas;
  372. this.width = width = width || 300;
  373. this.height = height = height || 300;
  374. if(node) {
  375. const ctx = node.getContext('2d');
  376. canvas = new Canvas(ctx, this, true, node);
  377. this.canvasNode = node
  378. } else {
  379. // #ifdef MP-TOUTIAO
  380. dpr = !this.isPC ? devicePixelRatio : 1// 1.25
  381. // #endif
  382. // #ifndef MP-ALIPAY || MP-TOUTIAO
  383. dpr = this.isPC ? devicePixelRatio : 1
  384. // #endif
  385. // #ifdef MP-ALIPAY || MP-LARK
  386. dpr = devicePixelRatio
  387. // #endif
  388. // #ifdef WEB
  389. dpr = 1
  390. // #endif
  391. this.rect = res
  392. this.nodeWidth = width * dpr;
  393. this.nodeHeight = height * dpr;
  394. const ctx = uni.createCanvasContext(this.canvasId, this);
  395. canvas = new Canvas(ctx, this, false);
  396. }
  397. return { canvas, width, height, devicePixelRatio: dpr, node };
  398. } else {
  399. return {}
  400. }
  401. })
  402. // #endif
  403. },
  404. // #ifndef APP-NVUE
  405. getRelative(e, touches) {
  406. let { clientX, clientY } = e
  407. if(!(clientX && clientY) && touches && touches[0]) {
  408. clientX = touches[0].clientX
  409. clientY = touches[0].clientY
  410. }
  411. return {x: clientX - this.rect.left, y: clientY - this.rect.top, wheelDelta: e.wheelDelta || 0}
  412. },
  413. getTouch(e, touches) {
  414. const {x} = touches && touches[0] || {}
  415. const touch = x ? touches[0] : this.getRelative(e, touches);
  416. if(this.landscape) {
  417. [touch.x, touch.y] = [touch.y, this.height - touch.x]
  418. }
  419. return touch;
  420. },
  421. touchStart(e) {
  422. this.isDown = true
  423. const next = () => {
  424. const touches = convertTouchesToArray(e.touches)
  425. if(this.chart) {
  426. const touch = this.getTouch(e, touches)
  427. this.startX = touch.x
  428. this.startY = touch.y
  429. this.startT = new Date()
  430. const handler = this.chart.getZr().handler;
  431. dispatch.call(handler, 'mousedown', touch)
  432. dispatch.call(handler, 'mousemove', touch)
  433. handler.processGesture(wrapTouch(e), 'start');
  434. clearTimeout(this.endTimer);
  435. }
  436. }
  437. if(this.isPC) {
  438. getRect(`#${this.canvasId}`, {context: this}).then(res => {
  439. this.rect = res
  440. next()
  441. })
  442. return
  443. }
  444. next()
  445. },
  446. touchMove(e) {
  447. if(this.isPC && this.enableHover && !this.isDown) {this.isDown = true}
  448. const touches = convertTouchesToArray(e.touches)
  449. if (this.chart && this.isDown) {
  450. const handler = this.chart.getZr().handler;
  451. dispatch.call(handler, 'mousemove', this.getTouch(e, touches))
  452. handler.processGesture(wrapTouch(e), 'change');
  453. }
  454. },
  455. touchEnd(e) {
  456. this.isDown = false
  457. if (this.chart) {
  458. const touches = convertTouchesToArray(e.changedTouches)
  459. const {x} = touches && touches[0] || {}
  460. const touch = (x ? touches[0] : this.getRelative(e, touches)) || {};
  461. if(this.landscape) {
  462. [touch.x, touch.y] = [touch.y, this.height - touch.x]
  463. }
  464. const handler = this.chart.getZr().handler;
  465. const isClick = Math.abs(touch.x - this.startX) < 10 && new Date() - this.startT < 200;
  466. dispatch.call(handler, 'mouseup', touch)
  467. handler.processGesture(wrapTouch(e), 'end');
  468. if(isClick) {
  469. dispatch.call(handler, 'click', touch)
  470. } else {
  471. this.endTimer = setTimeout(() => {
  472. dispatch.call(handler, 'mousemove', {x: 999999999,y: 999999999});
  473. dispatch.call(handler, 'mouseup', {x: 999999999,y: 999999999});
  474. },50)
  475. }
  476. }
  477. },
  478. // #endif
  479. // #ifdef H5
  480. mousewheel(e){
  481. if(this.chart) {
  482. // dispatch.call(this.chart.getZr().handler, 'mousewheel', this.getTouch(e))
  483. }
  484. }
  485. // #endif
  486. }
  487. };
  488. </script>
  489. <style>
  490. .lime-echart {
  491. position: relative;
  492. /* #ifndef APP-NVUE */
  493. width: 100%;
  494. height: 100%;
  495. /* #endif */
  496. /* #ifdef APP-NVUE */
  497. flex: 1;
  498. /* #endif */
  499. }
  500. .lime-echart__canvas {
  501. /* #ifndef APP-NVUE */
  502. width: 100%;
  503. height: 100%;
  504. /* #endif */
  505. /* #ifdef APP-NVUE */
  506. flex: 1;
  507. /* #endif */
  508. }
  509. /* #ifndef APP-NVUE */
  510. .lime-echart__mask {
  511. position: absolute;
  512. width: 100%;
  513. height: 100%;
  514. left: 0;
  515. top: 0;
  516. z-index: 1;
  517. }
  518. /* #endif */
  519. </style>