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