2026-05-14 13:44:43 +03:30
|
|
|
|
<template>
|
2026-06-14 10:08:46 +03:30
|
|
|
|
<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">
|
2026-06-14 10:08:46 +03:30
|
|
|
|
<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>
|
|
|
|
|
|
</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>
|
2026-06-14 10:08:46 +03:30
|
|
|
|
<div ref="chartContainerRef" id="chart-timeline" class="w-full" :class="chartInnerClass">
|
2026-05-14 13:44:43 +03:30
|
|
|
|
<apexchart
|
2026-06-14 10:08:46 +03:30
|
|
|
|
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">
|
2026-06-14 10:08:46 +03:30
|
|
|
|
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
2026-05-14 13:44:43 +03:30
|
|
|
|
import { useConfigStore } from '@/store/configStore'
|
2026-06-14 10:08:46 +03:30
|
|
|
|
import { usePrice } from '@/utils/usePrice'
|
|
|
|
|
|
import { getProjectFontFamily } from '@/utils/config'
|
2026-06-17 19:02:40 +03:30
|
|
|
|
import { computeTightChartYBounds, amplifyChartDeviation, shouldAmplifyKatodMesChart } from '@/utils/chartPriceAxis'
|
|
|
|
|
|
import { useSettingsStore } from '@/store/settingsStore'
|
2026-06-14 10:08:46 +03:30
|
|
|
|
import { useElementSize } from '@vueuse/core'
|
2026-05-14 13:44:43 +03:30
|
|
|
|
|
2026-06-14 10:08:46 +03:30
|
|
|
|
import type { ChartMetalType } 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
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
|
updateData: [timeline: string]
|
2026-06-14 10:08:46 +03:30
|
|
|
|
'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-14 10:08:46 +03:30
|
|
|
|
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,
|
|
|
|
|
|
})
|
|
|
|
|
|
)
|
|
|
|
|
|
const displayChartSeries = computed(() => {
|
|
|
|
|
|
const baseSeries = props.priceChartSeries?.[0]
|
|
|
|
|
|
if (!baseSeries?.data?.length || !amplifyKatodChart.value) {
|
|
|
|
|
|
return props.priceChartSeries
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
...baseSeries,
|
|
|
|
|
|
data: amplifyChartDeviation(baseSeries.data as [number, number][]),
|
|
|
|
|
|
},
|
|
|
|
|
|
]
|
|
|
|
|
|
})
|
|
|
|
|
|
const chartYBounds = computed(() => {
|
|
|
|
|
|
if (!amplifyKatodChart.value) {
|
|
|
|
|
|
return {}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const prices =
|
|
|
|
|
|
displayChartSeries.value?.[0]?.data
|
|
|
|
|
|
?.map((point: [number, number]) => Number(point?.[1]))
|
|
|
|
|
|
.filter((value: number) => Number.isFinite(value)) ?? []
|
|
|
|
|
|
|
|
|
|
|
|
return computeTightChartYBounds(prices)
|
|
|
|
|
|
})
|
2026-06-14 10:08:46 +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
|
|
|
|
|
2026-05-31 16:27:59 +03:30
|
|
|
|
const chartOptions = computed(() => {
|
|
|
|
|
|
const fontFamily = getProjectFontFamily()
|
2026-06-17 19:02:40 +03:30
|
|
|
|
const yBounds = chartYBounds.value
|
2026-05-31 16:27:59 +03:30
|
|
|
|
|
|
|
|
|
|
return {
|
2026-06-14 10:08:46 +03:30
|
|
|
|
chart: {
|
|
|
|
|
|
id: 'نوسانات قیمت',
|
|
|
|
|
|
type: 'area',
|
|
|
|
|
|
height: 200,
|
|
|
|
|
|
toolbar: { show: false },
|
2026-06-17 19:02:40 +03:30
|
|
|
|
zoom: { autoScaleYaxis: !amplifyKatodChart.value },
|
2026-06-14 10:08:46 +03:30
|
|
|
|
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: {
|
|
|
|
|
|
width: amplifyKatodChart.value ? 2.5 : 2,
|
|
|
|
|
|
curve: amplifyKatodChart.value ? 'smooth' : 'straight',
|
|
|
|
|
|
},
|
2026-06-14 10:08:46 +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,
|
2026-06-14 10:08:46 +03:30
|
|
|
|
labels: {
|
2026-06-17 13:51:29 +03:30
|
|
|
|
formatter: (val: number) => usePrice(val, true, 'ه.ریال'),
|
2026-06-14 10:08:46 +03:30
|
|
|
|
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-17 19:02:40 +03:30
|
|
|
|
...(amplifyKatodChart.value
|
|
|
|
|
|
? {
|
|
|
|
|
|
custom: ({ dataPointIndex, w }: { dataPointIndex: number; w: { globals: { seriesX: number[][] } } }) => {
|
|
|
|
|
|
const timestamp = w.globals.seriesX[0]?.[dataPointIndex]
|
|
|
|
|
|
const realPrice = props.priceChartSeries?.[0]?.data?.[dataPointIndex]?.[1]
|
|
|
|
|
|
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',
|
|
|
|
|
|
})}`
|
|
|
|
|
|
|
|
|
|
|
|
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">${usePrice(realPrice, true, 'ه.ریال')}</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>`
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
: {
|
|
|
|
|
|
formatter: (val: number) => usePrice(val, true, 'ه.ریال'),
|
|
|
|
|
|
}),
|
2026-06-14 10:08:46 +03:30
|
|
|
|
style: { fontFamily },
|
|
|
|
|
|
},
|
|
|
|
|
|
colors: [config.config.color],
|
|
|
|
|
|
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-31 16:27:59 +03:30
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-05-14 13:44:43 +03:30
|
|
|
|
|
|
|
|
|
|
const handleUpdate = (timeline: string) => {
|
|
|
|
|
|
emit('updateData', timeline)
|
|
|
|
|
|
}
|
2026-06-14 10:08:46 +03:30
|
|
|
|
</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: 6.5rem;
|
|
|
|
|
|
padding: 0.4rem 0.75rem;
|
|
|
|
|
|
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>
|