Files
boursecu_user/src/views/base/home/components/PriceChart.vue
T

364 lines
11 KiB
Vue
Raw Normal View History

2026-05-14 13:44:43 +03:30
<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' : '',
]"
>
2026-05-14 13:44:43 +03:30
<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>
2026-06-23 16:02:41 +03:30
<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">
2026-05-14 13:44:43 +03:30
<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">
2026-05-14 13:44:43 +03:30
<apexchart
v-if="canRenderChart"
:key="`${layout}-${variant}-${selectedMetal}-${chartSelection}-${seriesLength}`"
type="area"
width="100%"
:height="chartHeight"
:options="chartOptions"
2026-06-17 19:02:40 +03:30
:series="displayChartSeries"
2026-05-14 13:44:43 +03:30
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, ref, watch } from 'vue'
2026-05-14 13:44:43 +03:30
import { useConfigStore } from '@/store/configStore'
import { usePrice } from '@/utils/usePrice'
2026-06-24 11:52:09 +03:30
import { getBoursecuChartColor, getProjectFontFamily } from '@/utils/config'
2026-06-18 19:09:50 +03:30
import {
computeKatodMesVisualYBounds,
prepareKatodMesChartPoints,
shouldAmplifyKatodMesChart,
shouldUseKatodVisualChartTransform,
} from '@/utils/chartPriceAxis'
2026-06-17 19:02:40 +03:30
import { useSettingsStore } from '@/store/settingsStore'
import { useElementSize } from '@vueuse/core'
2026-06-18 19:09:50 +03:30
import { price } from '@/utils/price'
2026-05-14 13:44:43 +03:30
import type { ChartMetalType } from '../composables/usePriceChart'
2026-06-18 19:09:50 +03:30
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')
2026-05-14 13:44:43 +03:30
2026-06-24 11:52:09 +03:30
const chartColor = computed(() => {
if (props.layout === 'boursecu') {
return getBoursecuChartColor()
}
return config.config?.color ?? '#0A1B37'
})
2026-05-14 13:44:43 +03:30
const emit = defineEmits<{
updateData: [timeline: string]
'update:selectedMetal': [metal: ChartMetalType]
2026-05-14 13:44:43 +03:30
}>()
const config = useConfigStore()
2026-06-17 19:02:40 +03:30
const settingsStore = useSettingsStore()
2026-06-23 16:02:41 +03:30
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)
2026-06-17 19:02:40 +03:30
const amplifyKatodChart = computed(() =>
shouldAmplifyKatodMesChart({
isBoursecu: props.layout === 'boursecu',
metalTypeSetting: settingsStore.metalTypeSettingValue,
})
)
2026-06-18 19:09:50 +03:30
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,
2026-06-23 16:02:41 +03:30
metalRatio: selectedMetalChartRatio.value,
2026-06-18 19:09:50 +03:30
})
})
2026-06-17 19:02:40 +03:30
const displayChartSeries = computed(() => {
const baseSeries = props.priceChartSeries?.[0]
if (!baseSeries?.data?.length || !amplifyKatodChart.value) {
return props.priceChartSeries
}
2026-06-18 19:09:50 +03:30
const originalData = baseSeries.data as [number, number][]
const { data: displayData, tooltipPrices, dayMeta, pointMeta } = prepareKatodMesChartPoints(originalData, {
timeline: props.chartSelection,
dayRange: katodDayRange.value,
})
2026-06-17 19:02:40 +03:30
return [
{
...baseSeries,
2026-06-18 19:09:50 +03:30
data: displayData,
meta: { tooltipPrices, dayMeta, pointMeta },
2026-06-17 19:02:40 +03:30
},
]
})
const chartYBounds = computed(() => {
2026-06-18 19:09:50 +03:30
if (!katodUsesVisualTransform.value) {
2026-06-17 19:02:40 +03:30
return {}
}
const prices =
displayChartSeries.value?.[0]?.data
?.map((point: [number, number]) => Number(point?.[1]))
.filter((value: number) => Number.isFinite(value)) ?? []
2026-06-18 19:09:50 +03:30
return computeKatodMesVisualYBounds(prices)
2026-06-17 19:02:40 +03:30
})
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
})
2026-05-14 13:44:43 +03:30
const chartOptions = computed(() => {
const fontFamily = getProjectFontFamily()
2026-06-17 19:02:40 +03:30
const yBounds = chartYBounds.value
return {
chart: {
id: 'نوسانات قیمت',
type: 'area',
height: 200,
toolbar: { show: false },
2026-06-18 19:09:50 +03:30
zoom: { autoScaleYaxis: !katodUsesVisualTransform.value },
redrawOnParentResize: true,
},
grid: {
padding: { top: 0, right: 0, bottom: 0, left: 0 },
yaxis: { lines: { show: false } },
},
2026-06-17 19:02:40 +03:30
stroke: {
2026-06-18 19:09:50 +03:30
width: katodUsesVisualTransform.value ? 2.5 : 2,
curve: 'straight',
2026-06-17 19:02:40 +03:30
},
dataLabels: {
enabled: false,
style: {
fontFamily,
},
},
yaxis: {
tickAmount: 8,
show: false,
2026-06-17 19:02:40 +03:30
min: yBounds.min,
max: yBounds.max,
labels: {
2026-06-17 13:51:29 +03:30
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',
})}`
},
},
2026-06-18 19:09:50 +03:30
...(katodUsesVisualTransform.value
2026-06-17 19:02:40 +03:30
? {
custom: ({ dataPointIndex, w }: { dataPointIndex: number; w: { globals: { seriesX: number[][] } } }) => {
const timestamp = w.globals.seriesX[0]?.[dataPointIndex]
2026-06-18 19:09:50 +03:30
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
2026-06-17 19:02:40 +03:30
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',
})}`
2026-06-18 19:09:50 +03:30
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>`
: ''
2026-06-17 19:02:40 +03:30
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}">
2026-06-18 19:09:50 +03:30
<span class="apexcharts-tooltip-text-y-value">${valueLabel}</span>
2026-06-17 19:02:40 +03:30
</span>
2026-06-18 19:09:50 +03:30
</div>${rangeLine}`
2026-06-17 19:02:40 +03:30
},
}
: {
formatter: (val: number) => usePrice(val, true, 'ه.ریال'),
}),
style: { fontFamily },
},
2026-06-24 11:52:09 +03:30
colors: [chartColor.value],
fill: {
type: 'gradient',
gradient: {
shadeIntensity: 1,
opacityFrom: 0.7,
opacityTo: 0.9,
stops: [0, 100],
},
2026-05-14 13:44:43 +03:30
},
}
})
2026-05-14 13:44:43 +03:30
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;
2026-06-23 16:02:41 +03:30
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>