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>قیمت لحظه‌ای بازار</span>
</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 class="dlanding-price-card__value">
@@ -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<string, Array<{ timestamp:
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() {
try {
const data = await useAxios.get<Price>('/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<Array<Array<number>>>('/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()
@@ -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 {