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 }}
+
@@ -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 {