This commit is contained in:
oneao committed 2026-04-24 17:28:11 +08:00
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 }
}