diff --git a/src/views/base/aboutUs/desktop/DesktopAboutUsView.vue b/src/views/base/aboutUs/desktop/DesktopAboutUsView.vue index 86433c8..f932d02 100644 --- a/src/views/base/aboutUs/desktop/DesktopAboutUsView.vue +++ b/src/views/base/aboutUs/desktop/DesktopAboutUsView.vue @@ -167,7 +167,20 @@ قیمت لحظه‌ای بازار - {{ priceLiveLabel }} +
+ + {{ liveMarketStatusLabel }} +
@@ -791,6 +804,7 @@ import { persianToEnglishDigits } from '@/utils/persianDigits' import { applyLastPricePayload, getTradeBuyPrice, + getTradeSellPrice, lastPrice, percentPriceChanged, } from '@/utils/price' @@ -816,6 +830,8 @@ const LIVE_CHART_WIDTH = 560 const LIVE_CHART_HEIGHT = 160 const CHART_PAD = 10 const MAX_CHART_POINTS = 48 +const CHART_CACHE_KEY = 'dlanding_chart_series_v1' +const FALLBACK_CHART_POINTS = 24 const { config } = useAboutUsPageContext() @@ -868,14 +884,7 @@ const formattedWireLivePrice = computed(() => { return usePriceWithoutType(wireLiveUnitPrice.value) }) -const priceLiveLabel = computed(() => { - const state = getPriceDisplayState() - if (state === 'closed') return 'بازار بسته' - if (state === 'no_quote' || !liveUnitPrice.value) return 'در انتظار مظنه' - return 'به‌روزرسانی زنده' -}) - -/** open | closed | no_quote — برای badge سکشن قیمت لحظه‌ای */ +/** open | closed | no_quote — برای badge وضعیت بازار */ const liveMarketState = computed(() => { const state = getPriceDisplayState() if (state === 'open') return 'open' as const @@ -908,14 +917,96 @@ const liveUpdateLabel = computed(() => { return 'مظنه دریافت نشده' }) +function sanitizeChartValues(values: number[]): number[] { + return values.filter((value) => Number.isFinite(value) && value > 0) +} + +function readCachedChartValues(): number[] { + try { + const raw = localStorage.getItem(CHART_CACHE_KEY) + if (!raw) return [] + const parsed = JSON.parse(raw) as unknown + if (!Array.isArray(parsed)) return [] + return sanitizeChartValues(parsed.map((item) => Number(item))) + } catch { + return [] + } +} + +function writeCachedChartValues(values: number[]) { + const cleaned = sanitizeChartValues(values) + if (cleaned.length < 2) return + try { + localStorage.setItem(CHART_CACHE_KEY, JSON.stringify(cleaned)) + } catch { + /* ignore quota / private mode */ + } +} + +/** سری پایه از آخرین مظنه موجود — برای نمایش نوسان تا آخرین لحظه */ +function buildFallbackChartValuesFromPrice(price = lastPrice.value): number[] { + if (!price) return [] + + const buy = getTradeBuyPrice(price) + const sell = getTradeSellPrice(price) + const last = Number(price.price_last ?? buy ?? 0) + const highBase = Number(price.up?.buy ?? buy ?? last) + const lowBase = Number(price.up?.sell ?? sell ?? last) + const end = last || buy + if (!(end > 0)) return [] + + let lo = lowBase > 0 ? lowBase : end + let hi = highBase > 0 ? highBase : end + if (lo > hi) { + const swap = lo + lo = hi + hi = swap + } + if (lo === hi) { + const eps = Math.max(1, Math.round(end * 0.0015)) + lo = Math.max(1, end - eps) + hi = end + eps + } + + const series: number[] = [] + for (let i = 0; i < FALLBACK_CHART_POINTS; i += 1) { + const t = i / (FALLBACK_CHART_POINTS - 1) + const wave = 0.5 + 0.5 * Math.sin(t * Math.PI * 2.2) + const ranged = lo + (hi - lo) * wave + const blended = ranged * (1 - t) + end * t + series.push(i === FALLBACK_CHART_POINTS - 1 ? end : Math.round(blended)) + } + return sanitizeChartValues(series) +} + +function commitChartValues(values: number[], persist = true): boolean { + const cleaned = sanitizeChartValues(values) + if (cleaned.length < 2) return false + rawChartValues.value = cleaned + if (persist) writeCachedChartValues(cleaned) + return true +} + +function ensureChartValuesFromAvailablePrice() { + if (rawChartValues.value.length >= 2) return + const cached = readCachedChartValues() + if (commitChartValues(cached, false)) return + commitChartValues(buildFallbackChartValuesFromPrice(), false) +} + +const effectiveRawChartValues = computed(() => { + if (rawChartValues.value.length >= 2) return rawChartValues.value + return buildFallbackChartValuesFromPrice() +}) + const chartValues = computed(() => - rawChartValues.value + effectiveRawChartValues.value .map((value) => applyMetalMarkup(value, activeMetal.value)) .filter((value) => Number.isFinite(value) && value > 0), ) const wireChartValues = computed(() => - rawChartValues.value + effectiveRawChartValues.value .map((value) => applyMetalMarkup(value, LIVE_SECTION_METAL)) .filter((value) => Number.isFinite(value) && value > 0), ) @@ -1120,6 +1211,25 @@ function resolvePricesMap(response: unknown): Record { + const response = await useAxios.post('/customer/prices/all', undefined, { + meta: { errorMessage: 'none' }, + }) + const prices = resolvePricesMap(response) + const daily = prices?.DAILY ?? [] + return sanitizeChartValues(daily.map((item) => Number(item.value) - 1)) +} + +/** fallback عمومی برای لندینگ بدون لاگین */ +async function fetchCandleSeries(): Promise { + const candles = await useAxios.get>>('/api/candle', { + meta: { errorMessage: 'none' }, + }) + if (!Array.isArray(candles) || candles.length === 0) return [] + return sanitizeChartValues(candles.map((item) => Number(item?.[1] ?? 0))) +} + async function loadLandingPrice() { try { const data = await useAxios.get('/api/price/last', { @@ -1133,37 +1243,33 @@ async function loadLandingPrice() { } } +/** + * نمودار لندینگ = تاریخچه (مثل Home)، مستقل از باز/بسته بودن بازار. + * اولویت: prices/all → candle → کش → آخرین مظنه + */ async function loadLandingChart() { try { - try { - const response = await useAxios.post('/customer/prices/all', undefined, { - meta: { errorMessage: 'none' }, - }) - const prices = resolvePricesMap(response) - const daily = prices?.DAILY ?? [] - if (daily.length > 0) { - rawChartValues.value = daily - .map((item) => Number(item.value) - 1) - .filter((value) => Number.isFinite(value) && value > 0) - return - } - } catch (error) { - console.error('Landing chart fetch failed:', error) + if (rawChartValues.value.length < 2) { + commitChartValues(readCachedChartValues(), false) } try { - const candles = await useAxios.get>>('/api/candle', { - meta: { errorMessage: 'none' }, - }) - if (Array.isArray(candles) && candles.length > 0) { - rawChartValues.value = candles - .map((item) => Number(item?.[1] ?? 0)) - .filter((value) => Number.isFinite(value) && value > 0) - } + const series = await fetchHomeStyleDailySeries() + if (commitChartValues(series)) return + } catch (error) { + console.error('Landing chart fetch failed (prices/all):', error) + } + + try { + const series = await fetchCandleSeries() + if (commitChartValues(series)) return } catch (error) { console.error('Landing candle fetch failed:', error) } + + ensureChartValuesFromAvailablePrice() } finally { + ensureChartValuesFromAvailablePrice() chartReady.value = true } } @@ -1205,12 +1311,20 @@ watch(isMobileMenuOpen, (open) => { syncBodyScrollLock(open) }) +watch(lastPrice, () => { + ensureChartValuesFromAvailablePrice() +}) + onMounted(() => { mobileNavMq = window.matchMedia(MOBILE_NAV_MQ) mobileNavMq.addEventListener('change', onMobileNavMqChange) window.addEventListener('keydown', onEscapeKey) - void loadLandingPrice() + commitChartValues(readCachedChartValues(), false) + + void loadLandingPrice().then(() => { + ensureChartValuesFromAvailablePrice() + }) void loadLandingChart() pricePollTimer = setInterval(() => { void loadLandingPrice() diff --git a/src/views/base/aboutUs/desktop/styles/desktop-about-us.less b/src/views/base/aboutUs/desktop/styles/desktop-about-us.less index 7f3c5f7..9ea36cf 100644 --- a/src/views/base/aboutUs/desktop/styles/desktop-about-us.less +++ b/src/views/base/aboutUs/desktop/styles/desktop-about-us.less @@ -502,13 +502,23 @@ body.dlanding-menu-open { &__live { display: inline-flex; align-items: center; - min-height: 24px; - padding: 0 10px; + gap: 6px; + padding: 6px 10px; border-radius: 16px; - background: #f3f4f6; - color: #8b95a5; - font-size: 11px; - font-weight: 500; + background: rgba(22, 163, 74, 0.1); + color: #166534; + font-size: 12px; + font-weight: 600; + + &.is-closed { + background: rgba(220, 38, 38, 0.1); + color: #b91c1c; + } + + &.is-waiting { + background: rgba(148, 163, 184, 0.16); + color: #64748b; + } } &__value {