364 lines
11 KiB
Vue
364 lines
11 KiB
Vue
<template>
|
||
<div
|
||
class="bg-gray-50 w-full rounded-lg py-4"
|
||
:class="[
|
||
chartContainerClass,
|
||
variant === 'desktop' ? 'mt-0' : 'mt-4',
|
||
layout === 'boursecu' && variant !== 'desktop' ? 'price-chart-box--boursecu' : '',
|
||
]"
|
||
>
|
||
<div class="w-full text-center pt-2">
|
||
<div v-if="showMetalSelector" class="chart-metal-segment mb-3">
|
||
<button
|
||
type="button"
|
||
class="chart-metal-segment__item"
|
||
:class="{ 'chart-metal-segment__item--active': selectedMetal === 'katod' }"
|
||
@click="emit('update:selectedMetal', 'katod')"
|
||
>
|
||
کاتد مس
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="chart-metal-segment__item"
|
||
:class="{ 'chart-metal-segment__item--active': selectedMetal === 'maftol' }"
|
||
@click="emit('update:selectedMetal', 'maftol')"
|
||
>
|
||
مفتول مس
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="chart-metal-segment__item"
|
||
:class="{ 'chart-metal-segment__item--active': selectedMetal === 'shamsh' }"
|
||
@click="emit('update:selectedMetal', 'shamsh')"
|
||
>
|
||
شمش مس
|
||
</button>
|
||
</div>
|
||
<a-radio-group v-model:value="selectedValue" button-style="solid" class="chart-timeline-group">
|
||
<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 ref="chartContainerRef" id="chart-timeline" class="w-full" :class="chartInnerClass">
|
||
<apexchart
|
||
v-if="canRenderChart"
|
||
:key="`${layout}-${variant}-${selectedMetal}-${chartSelection}-${seriesLength}`"
|
||
type="area"
|
||
width="100%"
|
||
:height="chartHeight"
|
||
:options="chartOptions"
|
||
:series="displayChartSeries"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||
import { useConfigStore } from '@/store/configStore'
|
||
import { usePrice } from '@/utils/usePrice'
|
||
import { getBoursecuChartColor, getProjectFontFamily } from '@/utils/config'
|
||
import {
|
||
computeKatodMesVisualYBounds,
|
||
prepareKatodMesChartPoints,
|
||
shouldAmplifyKatodMesChart,
|
||
shouldUseKatodVisualChartTransform,
|
||
} from '@/utils/chartPriceAxis'
|
||
import { useSettingsStore } from '@/store/settingsStore'
|
||
import { useElementSize } from '@vueuse/core'
|
||
import { price } from '@/utils/price'
|
||
|
||
import type { ChartMetalType } from '../composables/usePriceChart'
|
||
import { resolveMetalDayRangePrices, usePriceChart } from '../composables/usePriceChart'
|
||
|
||
const props = withDefaults(
|
||
defineProps<{
|
||
priceChartSeries: any[]
|
||
layout?: 'default' | 'boursecu'
|
||
variant?: 'mobile' | 'desktop'
|
||
chartSelection?: string
|
||
selectedMetal?: ChartMetalType
|
||
}>(),
|
||
{
|
||
layout: 'default',
|
||
variant: 'mobile',
|
||
chartSelection: 'DAILY',
|
||
selectedMetal: 'katod',
|
||
}
|
||
)
|
||
|
||
const chartHeight = computed(() => (props.variant === 'desktop' ? 320 : 200))
|
||
const chartContainerClass = computed(() => {
|
||
if (props.variant === 'desktop') {
|
||
return 'min-h-[22rem]'
|
||
}
|
||
return props.layout === 'default' ? 'min-h-[15rem] h-[15rem]' : 'min-h-[15rem]'
|
||
})
|
||
const chartInnerClass = computed(() =>
|
||
props.variant === 'desktop' ? 'min-h-[320px]' : 'min-h-[200px]'
|
||
)
|
||
|
||
const showMetalSelector = computed(() => props.layout === 'boursecu')
|
||
|
||
const chartColor = computed(() => {
|
||
if (props.layout === 'boursecu') {
|
||
return getBoursecuChartColor()
|
||
}
|
||
|
||
return config.config?.color ?? '#0A1B37'
|
||
})
|
||
|
||
const emit = defineEmits<{
|
||
updateData: [timeline: string]
|
||
'update:selectedMetal': [metal: ChartMetalType]
|
||
}>()
|
||
|
||
const config = useConfigStore()
|
||
const settingsStore = useSettingsStore()
|
||
const { selectedMetalChartRatio } = usePriceChart()
|
||
const chartContainerRef = ref<HTMLElement | null>(null)
|
||
const chartMounted = ref(false)
|
||
const selectedValue = ref(props.chartSelection)
|
||
|
||
const { width: chartWidth } = useElementSize(chartContainerRef)
|
||
|
||
const seriesLength = computed(() => props.priceChartSeries?.[0]?.data?.length ?? 0)
|
||
const amplifyKatodChart = computed(() =>
|
||
shouldAmplifyKatodMesChart({
|
||
isBoursecu: props.layout === 'boursecu',
|
||
metalTypeSetting: settingsStore.metalTypeSettingValue,
|
||
})
|
||
)
|
||
const katodUsesVisualTransform = computed(
|
||
() => amplifyKatodChart.value && shouldUseKatodVisualChartTransform(props.chartSelection)
|
||
)
|
||
const katodDayRange = computed(() => {
|
||
if (!amplifyKatodChart.value) {
|
||
return undefined
|
||
}
|
||
|
||
return resolveMetalDayRangePrices({
|
||
price: price.value,
|
||
metal: props.selectedMetal,
|
||
isBoursecuLayout: true,
|
||
metalRatio: selectedMetalChartRatio.value,
|
||
})
|
||
})
|
||
const displayChartSeries = computed(() => {
|
||
const baseSeries = props.priceChartSeries?.[0]
|
||
if (!baseSeries?.data?.length || !amplifyKatodChart.value) {
|
||
return props.priceChartSeries
|
||
}
|
||
|
||
const originalData = baseSeries.data as [number, number][]
|
||
const { data: displayData, tooltipPrices, dayMeta, pointMeta } = prepareKatodMesChartPoints(originalData, {
|
||
timeline: props.chartSelection,
|
||
dayRange: katodDayRange.value,
|
||
})
|
||
|
||
return [
|
||
{
|
||
...baseSeries,
|
||
data: displayData,
|
||
meta: { tooltipPrices, dayMeta, pointMeta },
|
||
},
|
||
]
|
||
})
|
||
const chartYBounds = computed(() => {
|
||
if (!katodUsesVisualTransform.value) {
|
||
return {}
|
||
}
|
||
|
||
const prices =
|
||
displayChartSeries.value?.[0]?.data
|
||
?.map((point: [number, number]) => Number(point?.[1]))
|
||
.filter((value: number) => Number.isFinite(value)) ?? []
|
||
|
||
return computeKatodMesVisualYBounds(prices)
|
||
})
|
||
const canRenderChart = computed(
|
||
() => chartMounted.value && chartWidth.value > 0 && seriesLength.value > 0
|
||
)
|
||
|
||
watch(
|
||
() => props.chartSelection,
|
||
(timeline) => {
|
||
selectedValue.value = timeline
|
||
}
|
||
)
|
||
|
||
onMounted(async () => {
|
||
await nextTick()
|
||
chartMounted.value = true
|
||
})
|
||
|
||
const chartOptions = computed(() => {
|
||
const fontFamily = getProjectFontFamily()
|
||
const yBounds = chartYBounds.value
|
||
|
||
return {
|
||
chart: {
|
||
id: 'نوسانات قیمت',
|
||
type: 'area',
|
||
height: 200,
|
||
toolbar: { show: false },
|
||
zoom: { autoScaleYaxis: !katodUsesVisualTransform.value },
|
||
redrawOnParentResize: true,
|
||
},
|
||
grid: {
|
||
padding: { top: 0, right: 0, bottom: 0, left: 0 },
|
||
yaxis: { lines: { show: false } },
|
||
},
|
||
stroke: {
|
||
width: katodUsesVisualTransform.value ? 2.5 : 2,
|
||
curve: 'straight',
|
||
},
|
||
dataLabels: {
|
||
enabled: false,
|
||
style: {
|
||
fontFamily,
|
||
},
|
||
},
|
||
yaxis: {
|
||
tickAmount: 8,
|
||
show: false,
|
||
min: yBounds.min,
|
||
max: yBounds.max,
|
||
labels: {
|
||
formatter: (val: number) => usePrice(val, true, 'ه.ریال'),
|
||
style: { fontSize: '13px', fontFamily },
|
||
},
|
||
},
|
||
xaxis: {
|
||
show: false,
|
||
type: 'datetime',
|
||
axisTicks: { show: false },
|
||
tooltip: { enabled: false },
|
||
labels: { show: false },
|
||
},
|
||
tooltip: {
|
||
x: {
|
||
formatter: (value: number) => {
|
||
const date = new Date(value)
|
||
return `${date.toLocaleDateString('fa-IR')} - ${date.toLocaleTimeString('fa-IR', {
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
})}`
|
||
},
|
||
},
|
||
...(katodUsesVisualTransform.value
|
||
? {
|
||
custom: ({ dataPointIndex, w }: { dataPointIndex: number; w: { globals: { seriesX: number[][] } } }) => {
|
||
const timestamp = w.globals.seriesX[0]?.[dataPointIndex]
|
||
const seriesMeta = displayChartSeries.value?.[0]?.meta as
|
||
| {
|
||
tooltipPrices?: number[]
|
||
dayMeta?: { high: number; low: number; avg: number }[]
|
||
pointMeta?: { kind: 'high' | 'low' | 'avg'; dayHigh: number; dayLow: number }[]
|
||
}
|
||
| undefined
|
||
const pointMeta = seriesMeta?.pointMeta?.[dataPointIndex]
|
||
const dayMeta = seriesMeta?.dayMeta?.[dataPointIndex]
|
||
const realPrice = seriesMeta?.tooltipPrices?.[dataPointIndex] ?? dayMeta?.avg
|
||
if (timestamp == null || realPrice == null) {
|
||
return ''
|
||
}
|
||
|
||
const date = new Date(timestamp)
|
||
const timeLabel = `${date.toLocaleDateString('fa-IR')} - ${date.toLocaleTimeString('fa-IR', {
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
})}`
|
||
|
||
const priceLine = usePrice(realPrice, true, 'ه.ریال')
|
||
let valueLabel = priceLine
|
||
if (pointMeta?.kind === 'high') {
|
||
valueLabel = `بالاترین روز: ${priceLine}`
|
||
} else if (pointMeta?.kind === 'low') {
|
||
valueLabel = `پایینترین روز: ${priceLine}`
|
||
} else if (dayMeta) {
|
||
valueLabel = `میانگین روز: ${priceLine}`
|
||
}
|
||
|
||
const rangeHigh = pointMeta?.dayHigh ?? dayMeta?.high
|
||
const rangeLow = pointMeta?.dayLow ?? dayMeta?.low
|
||
const rangeLine =
|
||
rangeHigh != null && rangeLow != null
|
||
? `<div style="font-family:${fontFamily};font-size:12px;opacity:0.85;margin-top:4px">
|
||
بازه روز: ${usePrice(rangeHigh, true, 'ه.ریال')} — ${usePrice(rangeLow, true, 'ه.ریال')}
|
||
</div>`
|
||
: ''
|
||
|
||
return `<div class="apexcharts-tooltip-title" style="font-family:${fontFamily}">${timeLabel}</div>
|
||
<div class="apexcharts-tooltip-series-group apexcharts-active" style="order:1;display:flex">
|
||
<span class="apexcharts-tooltip-text" style="font-family:${fontFamily}">
|
||
<span class="apexcharts-tooltip-text-y-value">${valueLabel}</span>
|
||
</span>
|
||
</div>${rangeLine}`
|
||
},
|
||
}
|
||
: {
|
||
formatter: (val: number) => usePrice(val, true, 'ه.ریال'),
|
||
}),
|
||
style: { fontFamily },
|
||
},
|
||
colors: [chartColor.value],
|
||
fill: {
|
||
type: 'gradient',
|
||
gradient: {
|
||
shadeIntensity: 1,
|
||
opacityFrom: 0.7,
|
||
opacityTo: 0.9,
|
||
stops: [0, 100],
|
||
},
|
||
},
|
||
}
|
||
})
|
||
|
||
const handleUpdate = (timeline: string) => {
|
||
emit('updateData', timeline)
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="less">
|
||
@boursecu-surface-border: #d4e3de;
|
||
|
||
.price-chart-box--boursecu {
|
||
border: 2px solid @boursecu-surface-border;
|
||
}
|
||
|
||
.chart-metal-segment {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 2px;
|
||
padding: 3px;
|
||
border-radius: 10px;
|
||
background: #dfe8e5;
|
||
}
|
||
|
||
.chart-metal-segment__item {
|
||
appearance: none;
|
||
cursor: pointer;
|
||
min-width: 5.5rem;
|
||
padding: 0.4rem 0.6rem;
|
||
border: none;
|
||
border-radius: 8px;
|
||
background: transparent;
|
||
color: @App-primary-color;
|
||
opacity: 0.65;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
line-height: 1.25;
|
||
transition: background-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
|
||
}
|
||
|
||
.chart-metal-segment__item--active {
|
||
background: #fff;
|
||
color: @App-primary-color;
|
||
opacity: 1;
|
||
font-weight: 700;
|
||
box-shadow: 0 1px 4px rgba(10, 27, 55, 0.12);
|
||
}
|
||
</style>
|