202 lines
4.7 KiB
Vue
202 lines
4.7 KiB
Vue
<template>
|
||
<div class="bg-gray-50 w-full flex justify-evenly rounded-lg py-4 mt-4 h-[15rem]">
|
||
<div class="w-full text-center pt-2">
|
||
<a-radio-group v-model:value="selectedValue" button-style="solid">
|
||
<a-radio-button value="DAILY" @click="handleUpdate('DAILY')">24 ساعت اخیر</a-radio-button>
|
||
<a-radio-button value="WEEKLY" @click="handleUpdate('WEEKLY')">هفته گذشته</a-radio-button>
|
||
<a-radio-button value="MONTHLY" @click="handleUpdate('MONTHLY')">ماه گذشته</a-radio-button>
|
||
</a-radio-group>
|
||
<div class="min-h-[200px] relative">
|
||
<div v-if="hasChartData" ref="chartEl" class="h-[200px] w-full" />
|
||
<div
|
||
v-else
|
||
class="h-[200px] flex items-center justify-center text-gray-400 text-sm"
|
||
>
|
||
داده نمودار در دسترس نیست
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import * as echarts from 'echarts'
|
||
import type { EChartsOption } from 'echarts'
|
||
import { ref, computed, watch, onBeforeUnmount, nextTick } from 'vue'
|
||
import { useConfigStore } from '@/store/configStore'
|
||
import { usePrice } from '@/utils/usePrice'
|
||
|
||
type ChartPoint = [number, number]
|
||
|
||
const props = defineProps<{
|
||
priceChartSeries?: Array<{ name?: string; data?: unknown[] }> | null
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
updateData: [timeline: string]
|
||
}>()
|
||
|
||
const configStore = useConfigStore()
|
||
const selectedValue = ref('DAILY')
|
||
const chartEl = ref<HTMLElement | null>(null)
|
||
let chart: echarts.ECharts | null = null
|
||
let renderToken = 0
|
||
|
||
function extractPoints(): ChartPoint[] {
|
||
const source = props.priceChartSeries
|
||
if (!Array.isArray(source) || source.length === 0) {
|
||
return []
|
||
}
|
||
|
||
const raw = source[0]?.data
|
||
if (!Array.isArray(raw)) {
|
||
return []
|
||
}
|
||
|
||
return raw
|
||
.map((item) => {
|
||
if (!Array.isArray(item) || item.length < 2) {
|
||
return null
|
||
}
|
||
|
||
let x = Number(item[0])
|
||
let y = Number(item[1])
|
||
if (!Number.isFinite(x) || !Number.isFinite(y) || y <= 0) {
|
||
return null
|
||
}
|
||
|
||
if (x > 0 && x < 1e11) {
|
||
x *= 1000
|
||
}
|
||
|
||
return [x, y] as ChartPoint
|
||
})
|
||
.filter((point): point is ChartPoint => point !== null)
|
||
.sort((a, b) => a[0] - b[0])
|
||
}
|
||
|
||
const chartPoints = computed(() => extractPoints())
|
||
const hasChartData = computed(() => chartPoints.value.length >= 2)
|
||
|
||
const toPersianNumber = (num: number) =>
|
||
num.toString().replace(/\d/g, (d) => '۰۱۲۳۴۵۶۷۸۹'[d])
|
||
|
||
function buildOption(points: ChartPoint[]): EChartsOption {
|
||
const color = configStore.config?.color || '#CEA34C'
|
||
|
||
return {
|
||
animation: true,
|
||
grid: {
|
||
left: 4,
|
||
right: 4,
|
||
top: 8,
|
||
bottom: 4,
|
||
containLabel: false
|
||
},
|
||
xAxis: {
|
||
type: 'time',
|
||
show: false,
|
||
boundaryGap: false
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
show: false,
|
||
scale: true
|
||
},
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
confine: true,
|
||
formatter: (params) => {
|
||
const row = Array.isArray(params) ? params[0] : params
|
||
if (!row || !Array.isArray(row.value)) {
|
||
return ''
|
||
}
|
||
|
||
const [timestamp, value] = row.value as [number, number]
|
||
const date = new Date(timestamp)
|
||
const dateText = `${date.toLocaleDateString('fa-IR')} - ${date.toLocaleTimeString('fa-IR', {
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
})}`
|
||
|
||
return `${dateText}<br/>${toPersianNumber(usePrice(value, true, 'ه.ریال'))}`
|
||
}
|
||
},
|
||
series: [
|
||
{
|
||
name: 'قیمت',
|
||
type: 'line',
|
||
smooth: true,
|
||
showSymbol: false,
|
||
data: points,
|
||
lineStyle: {
|
||
width: 2,
|
||
color
|
||
},
|
||
areaStyle: {
|
||
color,
|
||
opacity: 0.25
|
||
}
|
||
}
|
||
]
|
||
}
|
||
}
|
||
|
||
function destroyChart() {
|
||
if (!chart) {
|
||
return
|
||
}
|
||
|
||
chart.dispose()
|
||
chart = null
|
||
}
|
||
|
||
async function renderChart() {
|
||
const token = ++renderToken
|
||
|
||
if (!hasChartData.value) {
|
||
destroyChart()
|
||
return
|
||
}
|
||
|
||
await nextTick()
|
||
await nextTick()
|
||
|
||
if (token !== renderToken || !chartEl.value) {
|
||
return
|
||
}
|
||
|
||
const points = chartPoints.value
|
||
|
||
try {
|
||
if (!chart || chart.isDisposed()) {
|
||
chart = echarts.init(chartEl.value, undefined, { renderer: 'canvas' })
|
||
}
|
||
|
||
chart.setOption(buildOption(points), true)
|
||
chart.resize()
|
||
} catch (error) {
|
||
console.error('Price chart render failed:', error)
|
||
destroyChart()
|
||
}
|
||
}
|
||
|
||
watch(
|
||
() => [chartPoints.value.length, selectedValue.value, configStore.config?.color] as const,
|
||
() => {
|
||
void renderChart()
|
||
},
|
||
{ immediate: true, flush: 'post' }
|
||
)
|
||
|
||
onBeforeUnmount(() => {
|
||
renderToken++
|
||
destroyChart()
|
||
})
|
||
|
||
const handleUpdate = (timeline: string) => {
|
||
selectedValue.value = timeline
|
||
emit('updateData', timeline)
|
||
}
|
||
</script>
|