u
This commit is contained in:
1 parent
07e0001b87
commit
46cbb2f50d
543 files changed
+32576
-4231
No files matched your search
@@ -0,0 +1,215 @@
|
||||
import type { BarSeriesOption, GaugeSeriesOption, LineSeriesOption, PictorialBarSeriesOption, PieSeriesOption, RadarSeriesOption, ScatterSeriesOption } from 'echarts/charts'
|
||||
import type { DatasetComponentOption, GridComponentOption, LegendComponentOption, TitleComponentOption, ToolboxComponentOption, TooltipComponentOption } from 'echarts/components'
|
||||
import { useElementSize } from '@vueuse/core'
|
||||
import { BarChart, GaugeChart, LineChart, PictorialBarChart, PieChart, RadarChart, ScatterChart } from 'echarts/charts'
|
||||
import { DatasetComponent, GridComponent, LegendComponent, TitleComponent, ToolboxComponent, TooltipComponent, TransformComponent } from 'echarts/components'
|
||||
import * as echarts from 'echarts/core'
|
||||
import { LabelLayout, UniversalTransition } from 'echarts/features'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
import { effectScope, nextTick, onScopeDispose, ref, watch } from 'vue'
|
||||
|
||||
export type ECOption = echarts.ComposeOption<
|
||||
| BarSeriesOption
|
||||
| LineSeriesOption
|
||||
| PieSeriesOption
|
||||
| ScatterSeriesOption
|
||||
| PictorialBarSeriesOption
|
||||
| RadarSeriesOption
|
||||
| GaugeSeriesOption
|
||||
| TitleComponentOption
|
||||
| LegendComponentOption
|
||||
| TooltipComponentOption
|
||||
| GridComponentOption
|
||||
| ToolboxComponentOption
|
||||
| DatasetComponentOption
|
||||
>
|
||||
|
||||
echarts.use([
|
||||
TitleComponent,
|
||||
LegendComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
DatasetComponent,
|
||||
TransformComponent,
|
||||
ToolboxComponent,
|
||||
BarChart,
|
||||
LineChart,
|
||||
PieChart,
|
||||
ScatterChart,
|
||||
PictorialBarChart,
|
||||
RadarChart,
|
||||
GaugeChart,
|
||||
LabelLayout,
|
||||
UniversalTransition,
|
||||
CanvasRenderer,
|
||||
])
|
||||
|
||||
interface ChartHooks {
|
||||
onRender?: (chart: echarts.ECharts) => void | Promise<void>
|
||||
onUpdated?: (chart: echarts.ECharts) => void | Promise<void>
|
||||
onDestroy?: (chart: echarts.ECharts) => void | Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* use echarts
|
||||
*
|
||||
* @param optionsFactory echarts options factory function
|
||||
* @param darkMode dark mode
|
||||
*/
|
||||
export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: ChartHooks = {}) {
|
||||
const scope = effectScope()
|
||||
|
||||
const darkMode = ref(false)
|
||||
|
||||
const domRef = ref<HTMLElement | null>(null)
|
||||
const initialSize = { width: 0, height: 0 }
|
||||
const { width, height } = useElementSize(domRef, initialSize)
|
||||
|
||||
let chart: echarts.ECharts | null = null
|
||||
const chartOptions: T = optionsFactory()
|
||||
|
||||
const {
|
||||
onRender = (instance) => {
|
||||
const textColor = darkMode.value ? 'rgb(224, 224, 224)' : 'rgb(31, 31, 31)'
|
||||
const maskColor = darkMode.value ? 'rgba(0, 0, 0, 0.4)' : 'rgba(255, 255, 255, 0.8)'
|
||||
|
||||
instance.showLoading({
|
||||
textColor,
|
||||
fontSize: 14,
|
||||
maskColor,
|
||||
})
|
||||
},
|
||||
onUpdated = (instance) => {
|
||||
instance.hideLoading()
|
||||
},
|
||||
onDestroy,
|
||||
} = hooks
|
||||
|
||||
/**
|
||||
* whether can render chart
|
||||
*
|
||||
* when domRef is ready and initialSize is valid
|
||||
*/
|
||||
function canRender() {
|
||||
return domRef.value && initialSize.width > 0 && initialSize.height > 0
|
||||
}
|
||||
|
||||
/** is chart rendered */
|
||||
function isRendered() {
|
||||
return Boolean(domRef.value && chart)
|
||||
}
|
||||
|
||||
/**
|
||||
* update chart options
|
||||
*
|
||||
* @param callback callback function
|
||||
*/
|
||||
async function updateOptions(callback: (opts: T, optsFactory: () => T) => ECOption = () => chartOptions) {
|
||||
if (!isRendered())
|
||||
return
|
||||
|
||||
const updatedOpts = callback(chartOptions, optionsFactory)
|
||||
|
||||
Object.assign(chartOptions, updatedOpts)
|
||||
|
||||
if (isRendered()) {
|
||||
chart?.clear()
|
||||
}
|
||||
|
||||
chart?.setOption({ ...updatedOpts, backgroundColor: 'transparent' })
|
||||
|
||||
await onUpdated?.(chart!)
|
||||
}
|
||||
|
||||
function setOptions(options: T) {
|
||||
chart?.setOption(options)
|
||||
}
|
||||
|
||||
/** render chart */
|
||||
async function render() {
|
||||
if (!isRendered()) {
|
||||
const chartTheme = darkMode.value ? 'dark' : 'light'
|
||||
|
||||
await nextTick()
|
||||
|
||||
chart = echarts.init(domRef.value, chartTheme)
|
||||
|
||||
chart.setOption({ ...chartOptions, backgroundColor: 'transparent' })
|
||||
|
||||
await onRender?.(chart)
|
||||
}
|
||||
}
|
||||
|
||||
/** resize chart */
|
||||
function resize() {
|
||||
chart?.resize()
|
||||
}
|
||||
|
||||
/** destroy chart */
|
||||
async function destroy() {
|
||||
if (!chart)
|
||||
return
|
||||
|
||||
await onDestroy?.(chart)
|
||||
chart?.dispose()
|
||||
chart = null
|
||||
}
|
||||
|
||||
/** change chart theme */
|
||||
async function changeTheme() {
|
||||
await destroy()
|
||||
await render()
|
||||
await onUpdated?.(chart!)
|
||||
}
|
||||
|
||||
/**
|
||||
* render chart by size
|
||||
*
|
||||
* @param w width
|
||||
* @param h height
|
||||
*/
|
||||
async function renderChartBySize(w: number, h: number) {
|
||||
initialSize.width = w
|
||||
initialSize.height = h
|
||||
|
||||
// size is abnormal, destroy chart
|
||||
if (!canRender()) {
|
||||
await destroy()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// resize chart
|
||||
if (isRendered()) {
|
||||
resize()
|
||||
}
|
||||
|
||||
// render chart
|
||||
await render()
|
||||
|
||||
if (chart) {
|
||||
await onUpdated?.(chart)
|
||||
}
|
||||
}
|
||||
|
||||
scope.run(() => {
|
||||
watch([width, height], ([newWidth, newHeight]) => {
|
||||
renderChartBySize(newWidth, newHeight)
|
||||
})
|
||||
|
||||
watch(darkMode, () => {
|
||||
changeTheme()
|
||||
})
|
||||
})
|
||||
|
||||
onScopeDispose(() => {
|
||||
destroy()
|
||||
scope.stop()
|
||||
})
|
||||
|
||||
return {
|
||||
domRef,
|
||||
updateOptions,
|
||||
setOptions,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import type { Ref } from 'vue'
|
||||
import { onUnmounted, watch } from 'vue'
|
||||
|
||||
interface UseScrollWatcherOptions {
|
||||
target: Ref<HTMLElement | null>
|
||||
thresholdPercent?: number
|
||||
delay?: number
|
||||
onReachTop?: () => void
|
||||
onReachBottom?: () => void
|
||||
}
|
||||
|
||||
export function useScrollWatcher({
|
||||
target,
|
||||
thresholdPercent = 0.05,
|
||||
delay = 0,
|
||||
onReachTop,
|
||||
onReachBottom,
|
||||
}: UseScrollWatcherOptions) {
|
||||
if (!target)
|
||||
throw new Error('target 必须传入一个 ref')
|
||||
|
||||
let lastScrollTop = 0
|
||||
let timeout: ReturnType<typeof setTimeout> | null = null
|
||||
const topTriggered = { value: false }
|
||||
const bottomTriggered = { value: false }
|
||||
|
||||
const handleScroll = (e: Event) => {
|
||||
const el = e.target as HTMLElement
|
||||
const scrollTop = el.scrollTop
|
||||
const scrollHeight = el.scrollHeight
|
||||
const clientHeight = el.clientHeight
|
||||
const scrollRange = scrollHeight - clientHeight
|
||||
if (scrollRange <= 0)
|
||||
return
|
||||
|
||||
const isScrollingUp = scrollTop < lastScrollTop
|
||||
const isScrollingDown = scrollTop > lastScrollTop
|
||||
lastScrollTop = scrollTop
|
||||
|
||||
const topThreshold = scrollRange * thresholdPercent
|
||||
const bottomThreshold = scrollRange * thresholdPercent
|
||||
|
||||
if (timeout)
|
||||
clearTimeout(timeout)
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
if (isScrollingUp && scrollTop <= topThreshold && !topTriggered.value) {
|
||||
onReachTop?.()
|
||||
topTriggered.value = true
|
||||
}
|
||||
else if (scrollTop > topThreshold) {
|
||||
topTriggered.value = false
|
||||
}
|
||||
|
||||
if (isScrollingDown && scrollHeight - scrollTop - clientHeight <= bottomThreshold && !bottomTriggered.value) {
|
||||
onReachBottom?.()
|
||||
bottomTriggered.value = true
|
||||
}
|
||||
else if (scrollHeight - scrollTop - clientHeight > bottomThreshold) {
|
||||
bottomTriggered.value = false
|
||||
}
|
||||
}, delay)
|
||||
}
|
||||
|
||||
const bindScroll = (el: HTMLElement | null) => {
|
||||
if (!el)
|
||||
return
|
||||
el.addEventListener('scroll', handleScroll)
|
||||
lastScrollTop = el.scrollTop
|
||||
}
|
||||
|
||||
const unbindScroll = (el: HTMLElement | null) => {
|
||||
if (!el)
|
||||
return
|
||||
el.removeEventListener('scroll', handleScroll)
|
||||
}
|
||||
|
||||
// 监听 ref
|
||||
watch(target, (el, oldEl) => {
|
||||
if (oldEl)
|
||||
unbindScroll(oldEl)
|
||||
if (el)
|
||||
bindScroll(el)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (target.value)
|
||||
unbindScroll(target.value)
|
||||
if (timeout)
|
||||
clearTimeout(timeout)
|
||||
})
|
||||
|
||||
const scrollToTop = () => {
|
||||
if (target.value)
|
||||
target.value.scrollTop = 0
|
||||
}
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (target.value) {
|
||||
const el = target.value
|
||||
el.scrollTop = el.scrollHeight - el.clientHeight
|
||||
}
|
||||
}
|
||||
|
||||
return { scrollToTop, scrollToBottom }
|
||||
}
|
||||
Reference in new issue
Block a user