update landing page
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user