update landing page

This commit is contained in:
2026-08-01 18:02:01 +03:30
parent 38fc537883
commit 2dd2d3a624
2 changed files with 164 additions and 40 deletions
@@ -167,7 +167,20 @@
<span class="dlanding-dot"></span> <span class="dlanding-dot"></span>
<span>قیمت لحظه‌ای بازار</span> <span>قیمت لحظه‌ای بازار</span>
</div> </div>
<span class="dlanding-price-card__live">{{ priceLiveLabel }}</span> <div
class="dlanding-price-card__live"
:class="{
'is-closed': liveMarketState === 'closed',
'is-waiting': liveMarketState === 'no_quote',
}"
>
<BasicIcon
:icon="liveMarketStatusIcon"
size="14"
:color="liveMarketStatusColor"
/>
<span>{{ liveMarketStatusLabel }}</span>
</div>
</div> </div>
<div class="dlanding-price-card__value"> <div class="dlanding-price-card__value">
@@ -791,6 +804,7 @@ import { persianToEnglishDigits } from '@/utils/persianDigits'
import { import {
applyLastPricePayload, applyLastPricePayload,
getTradeBuyPrice, getTradeBuyPrice,
getTradeSellPrice,
lastPrice, lastPrice,
percentPriceChanged, percentPriceChanged,
} from '@/utils/price' } from '@/utils/price'
@@ -816,6 +830,8 @@ const LIVE_CHART_WIDTH = 560
const LIVE_CHART_HEIGHT = 160 const LIVE_CHART_HEIGHT = 160
const CHART_PAD = 10 const CHART_PAD = 10
const MAX_CHART_POINTS = 48 const MAX_CHART_POINTS = 48
const CHART_CACHE_KEY = 'dlanding_chart_series_v1'
const FALLBACK_CHART_POINTS = 24
const { config } = useAboutUsPageContext() const { config } = useAboutUsPageContext()
@@ -868,14 +884,7 @@ const formattedWireLivePrice = computed(() => {
return usePriceWithoutType(wireLiveUnitPrice.value) return usePriceWithoutType(wireLiveUnitPrice.value)
}) })
const priceLiveLabel = computed(() => { /** open | closed | no_quote — برای badge وضعیت بازار */
const state = getPriceDisplayState()
if (state === 'closed') return 'بازار بسته'
if (state === 'no_quote' || !liveUnitPrice.value) return 'در انتظار مظنه'
return 'به‌روزرسانی زنده'
})
/** open | closed | no_quote — برای badge سکشن قیمت لحظه‌ای */
const liveMarketState = computed(() => { const liveMarketState = computed(() => {
const state = getPriceDisplayState() const state = getPriceDisplayState()
if (state === 'open') return 'open' as const if (state === 'open') return 'open' as const
@@ -908,14 +917,96 @@ const liveUpdateLabel = computed(() => {
return 'مظنه دریافت نشده' 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(() => const chartValues = computed(() =>
rawChartValues.value effectiveRawChartValues.value
.map((value) => applyMetalMarkup(value, activeMetal.value)) .map((value) => applyMetalMarkup(value, activeMetal.value))
.filter((value) => Number.isFinite(value) && value > 0), .filter((value) => Number.isFinite(value) && value > 0),
) )
const wireChartValues = computed(() => const wireChartValues = computed(() =>
rawChartValues.value effectiveRawChartValues.value
.map((value) => applyMetalMarkup(value, LIVE_SECTION_METAL)) .map((value) => applyMetalMarkup(value, LIVE_SECTION_METAL))
.filter((value) => Number.isFinite(value) && value > 0), .filter((value) => Number.isFinite(value) && value > 0),
) )
@@ -1120,6 +1211,25 @@ function resolvePricesMap(response: unknown): Record<string, Array<{ timestamp:
return null return null
} }
/** مثل Home: سری DAILY از /customer/prices/all با value - 1 */
async function fetchHomeStyleDailySeries(): Promise<number[]> {
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<number[]> {
const candles = await useAxios.get<Array<Array<number>>>('/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() { async function loadLandingPrice() {
try { try {
const data = await useAxios.get<Price>('/api/price/last', { const data = await useAxios.get<Price>('/api/price/last', {
@@ -1133,37 +1243,33 @@ async function loadLandingPrice() {
} }
} }
/**
* نمودار لندینگ = تاریخچه (مثل Home)، مستقل از باز/بسته بودن بازار.
* اولویت: prices/all → candle → کش → آخرین مظنه
*/
async function loadLandingChart() { async function loadLandingChart() {
try { try {
try { if (rawChartValues.value.length < 2) {
const response = await useAxios.post('/customer/prices/all', undefined, { commitChartValues(readCachedChartValues(), false)
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)
} }
try { try {
const candles = await useAxios.get<Array<Array<number>>>('/api/candle', { const series = await fetchHomeStyleDailySeries()
meta: { errorMessage: 'none' }, if (commitChartValues(series)) return
}) } catch (error) {
if (Array.isArray(candles) && candles.length > 0) { console.error('Landing chart fetch failed (prices/all):', error)
rawChartValues.value = candles }
.map((item) => Number(item?.[1] ?? 0))
.filter((value) => Number.isFinite(value) && value > 0) try {
} const series = await fetchCandleSeries()
if (commitChartValues(series)) return
} catch (error) { } catch (error) {
console.error('Landing candle fetch failed:', error) console.error('Landing candle fetch failed:', error)
} }
ensureChartValuesFromAvailablePrice()
} finally { } finally {
ensureChartValuesFromAvailablePrice()
chartReady.value = true chartReady.value = true
} }
} }
@@ -1205,12 +1311,20 @@ watch(isMobileMenuOpen, (open) => {
syncBodyScrollLock(open) syncBodyScrollLock(open)
}) })
watch(lastPrice, () => {
ensureChartValuesFromAvailablePrice()
})
onMounted(() => { onMounted(() => {
mobileNavMq = window.matchMedia(MOBILE_NAV_MQ) mobileNavMq = window.matchMedia(MOBILE_NAV_MQ)
mobileNavMq.addEventListener('change', onMobileNavMqChange) mobileNavMq.addEventListener('change', onMobileNavMqChange)
window.addEventListener('keydown', onEscapeKey) window.addEventListener('keydown', onEscapeKey)
void loadLandingPrice() commitChartValues(readCachedChartValues(), false)
void loadLandingPrice().then(() => {
ensureChartValuesFromAvailablePrice()
})
void loadLandingChart() void loadLandingChart()
pricePollTimer = setInterval(() => { pricePollTimer = setInterval(() => {
void loadLandingPrice() void loadLandingPrice()
@@ -502,13 +502,23 @@ body.dlanding-menu-open {
&__live { &__live {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
min-height: 24px; gap: 6px;
padding: 0 10px; padding: 6px 10px;
border-radius: 16px; border-radius: 16px;
background: #f3f4f6; background: rgba(22, 163, 74, 0.1);
color: #8b95a5; color: #166534;
font-size: 11px; font-size: 12px;
font-weight: 500; 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 { &__value {