Add support chat, piggy funds, PWA banner, notification/SSE sync, and messenger legacy guards while keeping boursecu theme; also balance login panels and unblock login navigation. Co-authored-by: Cursor <cursoragent@cursor.com>
226 lines
6.3 KiB
TypeScript
226 lines
6.3 KiB
TypeScript
import { computed, ref, watch } from 'vue'
|
|
import { getAllPrices } from '@/api/home'
|
|
import { getProductsCatalog, getProductsCatalogRef } from '@/composables/useProductsCatalog'
|
|
import { getTradeBuyPrice, getTradeSellPrice } from '@/utils/price'
|
|
|
|
export type ChartMetalType = 'katod' | 'maftol' | 'shamsh'
|
|
export type ChartTimeline = 'DAILY' | 'WEEKLY' | 'MONTHLY'
|
|
|
|
type ChartPoint = [number, number]
|
|
type PricesMap = Record<string, Array<{ timestamp: number; value: number }>>
|
|
|
|
/**
|
|
* مارکآپ ثابت روی قیمت بورس کالا برای نمودار:
|
|
* کاتد +۳٪ ، مفتول +۶٪ ، شمش +۱۲٪
|
|
*/
|
|
export const CHART_METAL_MARKUP_RATIO: Record<ChartMetalType, number> = {
|
|
katod: 0.03,
|
|
maftol: 0.06,
|
|
shamsh: 0.12,
|
|
}
|
|
|
|
/** Module-level chart state — one dataset shared by mobile and desktop PriceChart instances. */
|
|
const priceChartSeries = ref<{ name: string; data: ChartPoint[] }[]>([
|
|
{ name: 'قیمت', data: [] },
|
|
])
|
|
const rawChartPoints = ref<ChartPoint[]>([])
|
|
const cachedPrices = ref<PricesMap | null>(null)
|
|
const chartSelection = ref<ChartTimeline>('DAILY')
|
|
const chartLoading = ref(false)
|
|
const selectedChartMetal = ref<ChartMetalType>('katod')
|
|
const catalogProducts = getProductsCatalogRef()
|
|
|
|
const maftolChartRatio = computed(() => CHART_METAL_MARKUP_RATIO.maftol)
|
|
const shamshChartRatio = computed(() => CHART_METAL_MARKUP_RATIO.shamsh)
|
|
|
|
const selectedMetalChartRatio = computed(
|
|
() => CHART_METAL_MARKUP_RATIO[selectedChartMetal.value],
|
|
)
|
|
|
|
let chartFetchPromise: Promise<void> | null = null
|
|
let chartInitialized = false
|
|
let metalOffsetPromise: Promise<void> | null = null
|
|
let metalOffsetLoaded = false
|
|
|
|
function applyChartRatio(basePrice: number, ratio: number) {
|
|
if (!ratio) {
|
|
return basePrice
|
|
}
|
|
|
|
return Math.round(basePrice + basePrice * ratio)
|
|
}
|
|
|
|
/** بالاترین/پایینترین امروز — همان مارکآپ ثابت فلز روی قیمت بورس کالا */
|
|
export function resolveMetalDayRangePrices(options: {
|
|
price: { up?: { buy?: number; sell?: number }; buy?: number; sell?: number; price_last?: number } | null | undefined
|
|
metal: ChartMetalType
|
|
metalRatio: number
|
|
}) {
|
|
const baseHigh = options.price?.up?.buy ?? getTradeBuyPrice(options.price as Parameters<typeof getTradeBuyPrice>[0])
|
|
const baseLow = options.price?.up?.sell ?? getTradeSellPrice(options.price as Parameters<typeof getTradeSellPrice>[0])
|
|
const ratio = options.metalRatio || CHART_METAL_MARKUP_RATIO[options.metal]
|
|
|
|
return {
|
|
high: applyChartRatio(baseHigh, ratio),
|
|
low: applyChartRatio(baseLow, ratio),
|
|
}
|
|
}
|
|
|
|
function normalizeChartTimestamp(timestamp: number) {
|
|
return timestamp < 1_000_000_000_000 ? timestamp * 1000 : timestamp
|
|
}
|
|
|
|
function resolvePricesMap(response: unknown): PricesMap | null {
|
|
if (!response || typeof response !== 'object') {
|
|
return null
|
|
}
|
|
|
|
const body = response as Record<string, unknown>
|
|
const nested = body.data
|
|
|
|
if (nested && typeof nested === 'object' && 'prices' in nested) {
|
|
return (nested as { prices: PricesMap }).prices
|
|
}
|
|
|
|
if ('prices' in body) {
|
|
return body.prices as PricesMap
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
function buildChartPoints(items: Array<{ timestamp: number; value: number }>): ChartPoint[] {
|
|
return items.map((item) => [
|
|
normalizeChartTimestamp(item.timestamp),
|
|
item.value - 1,
|
|
])
|
|
}
|
|
|
|
function syncChartSeries() {
|
|
const ratio = selectedMetalChartRatio.value
|
|
// New array so Vue invalidates parent computeds (nested .data mutation did not).
|
|
priceChartSeries.value = [
|
|
{
|
|
name: 'قیمت',
|
|
data: rawChartPoints.value.map(
|
|
([timestamp, value]) => [timestamp, applyChartRatio(value, ratio)] as ChartPoint,
|
|
),
|
|
},
|
|
]
|
|
}
|
|
|
|
function applyTimelineFromCache(timeline: ChartTimeline = chartSelection.value) {
|
|
const selectedData = cachedPrices.value?.[timeline] ?? []
|
|
rawChartPoints.value = buildChartPoints(selectedData)
|
|
syncChartSeries()
|
|
}
|
|
|
|
watch([selectedChartMetal, maftolChartRatio, shamshChartRatio], () => {
|
|
syncChartSeries()
|
|
})
|
|
|
|
async function fetchPrices(force = false) {
|
|
if (!force && chartInitialized && cachedPrices.value) {
|
|
applyTimelineFromCache()
|
|
return
|
|
}
|
|
|
|
if (!force && chartFetchPromise) {
|
|
return chartFetchPromise
|
|
}
|
|
|
|
chartFetchPromise = (async () => {
|
|
try {
|
|
chartLoading.value = true
|
|
const response = await getAllPrices()
|
|
const data = resolvePricesMap(response)
|
|
cachedPrices.value = data
|
|
applyTimelineFromCache()
|
|
chartInitialized = Boolean(data)
|
|
} catch (error) {
|
|
console.error('Error fetching prices:', error)
|
|
} finally {
|
|
chartLoading.value = false
|
|
chartFetchPromise = null
|
|
}
|
|
})()
|
|
|
|
return chartFetchPromise
|
|
}
|
|
|
|
async function loadMetalChartOffsets(force = false) {
|
|
if (!force && metalOffsetLoaded && catalogProducts.value.length > 0) {
|
|
return
|
|
}
|
|
|
|
if (!force && metalOffsetPromise) {
|
|
return metalOffsetPromise
|
|
}
|
|
|
|
metalOffsetPromise = (async () => {
|
|
try {
|
|
await getProductsCatalog(force)
|
|
syncChartSeries()
|
|
metalOffsetLoaded = true
|
|
} catch (error) {
|
|
console.error('Error loading boursecu chart metal ratios:', error)
|
|
syncChartSeries()
|
|
} finally {
|
|
metalOffsetPromise = null
|
|
}
|
|
})()
|
|
|
|
return metalOffsetPromise
|
|
}
|
|
|
|
function updateChartData(timeline: string) {
|
|
chartSelection.value = timeline as ChartTimeline
|
|
if (cachedPrices.value) {
|
|
applyTimelineFromCache(chartSelection.value)
|
|
return
|
|
}
|
|
void fetchPrices(true)
|
|
}
|
|
|
|
function setChartMetal(metal: ChartMetalType) {
|
|
selectedChartMetal.value = metal
|
|
syncChartSeries()
|
|
}
|
|
|
|
export function usePriceChart() {
|
|
return {
|
|
priceChartSeries,
|
|
chartSelection,
|
|
chartLoading,
|
|
selectedChartMetal,
|
|
maftolChartRatio,
|
|
shamshChartRatio,
|
|
selectedMetalChartRatio,
|
|
updateChartData,
|
|
fetchPrices,
|
|
loadMaftolOffset: loadMetalChartOffsets,
|
|
loadMetalChartOffsets,
|
|
setChartMetal,
|
|
}
|
|
}
|
|
|
|
export function fetchHomeChartPrices(force = false) {
|
|
return fetchPrices(force)
|
|
}
|
|
|
|
export function loadHomeMaftolOffset(force = false) {
|
|
return loadMetalChartOffsets(force)
|
|
}
|
|
|
|
export function resetHomePriceChart() {
|
|
priceChartSeries.value = [{ name: 'قیمت', data: [] }]
|
|
rawChartPoints.value = []
|
|
cachedPrices.value = null
|
|
chartSelection.value = 'DAILY'
|
|
selectedChartMetal.value = 'katod'
|
|
chartInitialized = false
|
|
chartFetchPromise = null
|
|
metalOffsetLoaded = false
|
|
metalOffsetPromise = null
|
|
}
|