fix(home): show quote skeleton while loading and harden auth/images
Prefer server quote status, avoid false no-quote before ready, toast on 401 re-login, and keep product/banner image URLs absolute. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -17,6 +17,8 @@ let lastSubscribers = 0
|
||||
|
||||
export const price = ref<Price>()
|
||||
export const lastPrice = ref<Price>()
|
||||
/** حداقل یک بار تلاش برای گرفتن مظنه تمام شده (موفق یا ناموفق) */
|
||||
export const lastPriceReady = ref(false)
|
||||
export const olderPrice = ref<number | null>(null)
|
||||
|
||||
let lastPriceSourceAt = 0
|
||||
@@ -180,6 +182,8 @@ export async function getLastPrices(): Promise<boolean> {
|
||||
console.log(error)
|
||||
markPriceSourceFetchFailed()
|
||||
return false
|
||||
} finally {
|
||||
lastPriceReady.value = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,6 +198,7 @@ export function applyLastPricePayload(dataLast: Price) {
|
||||
}
|
||||
|
||||
lastPrice.value = normalized
|
||||
lastPriceReady.value = true
|
||||
price.value = {
|
||||
...normalized,
|
||||
buy: nextBuy,
|
||||
|
||||
@@ -20,11 +20,51 @@ function isUsableImageSrc(src?: string | null): boolean {
|
||||
}
|
||||
|
||||
function absolutizeImageUrl(url: string): string {
|
||||
if (/^https?:\/\//i.test(url)) return url
|
||||
const base = String(getConfigApp('node_server') || '').replace(/\/$/, '')
|
||||
const path = url.replace(/^\//, '')
|
||||
if (!base) return url.startsWith('/') ? url : `/${url}`
|
||||
return `${base}/${path}`
|
||||
const normalized = normalizePublicFileUrl(url)
|
||||
return normalized ?? url
|
||||
}
|
||||
|
||||
function projectBase(): string {
|
||||
return String(getConfigApp('node_server') || '').replace(/\/$/, '')
|
||||
}
|
||||
|
||||
function storageRelativePath(value: string): string | null {
|
||||
const match = value.match(/(?:^|\/)(storage\/.+)$/i)
|
||||
return match ? match[1].replace(/^\/+/, '') : null
|
||||
}
|
||||
|
||||
function joinProjectUrl(relative: string): string {
|
||||
const path = relative.replace(/^\//, '')
|
||||
const base = projectBase()
|
||||
if (!base) return `/${path}`
|
||||
let url = `${base}/${path}`
|
||||
if (typeof window !== 'undefined' && window.location.protocol === 'https:') {
|
||||
url = url.replace(/^http:\/\//i, 'https://')
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
export function normalizePublicFileUrl(raw?: string | null): string | null {
|
||||
if (raw == null) return null
|
||||
const trimmed = String(raw).trim()
|
||||
if (!trimmed || trimmed.toLowerCase() === 'null' || trimmed.toLowerCase() === 'undefined') {
|
||||
return null
|
||||
}
|
||||
if (/^(data:|blob:)/i.test(trimmed)) return trimmed
|
||||
|
||||
const storagePath = storageRelativePath(trimmed)
|
||||
if (storagePath && projectBase()) {
|
||||
return joinProjectUrl(storagePath)
|
||||
}
|
||||
|
||||
if (/^https?:\/\//i.test(trimmed)) {
|
||||
if (typeof window !== 'undefined' && window.location.protocol === 'https:') {
|
||||
return trimmed.replace(/^http:\/\//i, 'https://')
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
return joinProjectUrl(trimmed)
|
||||
}
|
||||
|
||||
export function getProductFallbackImage(product?: ProductImageSource): string {
|
||||
|
||||
+17
-1
@@ -1,6 +1,6 @@
|
||||
import { ref } from 'vue'
|
||||
import { useSettingsStore } from '@/store/settingsStore'
|
||||
import { lastPrice } from '@/utils/price'
|
||||
import { lastPrice, lastPriceReady } from '@/utils/price'
|
||||
|
||||
export interface StoreTradingHours {
|
||||
start: string
|
||||
@@ -298,6 +298,11 @@ export function getPriceDisplayState(): PriceDisplayState {
|
||||
void useSettingsStore().settingsRevision
|
||||
const p = lastPrice.value
|
||||
|
||||
const serverStatus = p?.status
|
||||
if (serverStatus === 'open' || serverStatus === 'closed' || serverStatus === 'no_quote') {
|
||||
return serverStatus
|
||||
}
|
||||
|
||||
if (isOutsideGlobalStoreHours()) {
|
||||
if (!shouldShowHomePriceWhenClosed()) return 'closed'
|
||||
return hasShowableHomePrice(p) ? 'open' : 'no_quote'
|
||||
@@ -309,6 +314,17 @@ export function getPriceDisplayState(): PriceDisplayState {
|
||||
return 'no_quote'
|
||||
}
|
||||
|
||||
/** اولین دریافت مظنه / تنظیمات هنوز تمام نشده — اسکلتون، نه «مظنه در دسترس نیست» */
|
||||
export function isHomeQuotePending(): boolean {
|
||||
const settings = useSettingsStore()
|
||||
void lastPriceReady.value
|
||||
void lastPrice.value
|
||||
void settings.loaded
|
||||
void settings.settingsRevision
|
||||
|
||||
return !settings.loaded || !lastPriceReady.value
|
||||
}
|
||||
|
||||
export function getPriceDisplayMessage(state: PriceDisplayState = getPriceDisplayState()): string {
|
||||
if (state === 'closed') {
|
||||
return getStoreClosedMessage()
|
||||
|
||||
@@ -60,6 +60,7 @@ _axios.interceptors.response.use(
|
||||
|
||||
if (response?.status === 401) {
|
||||
removeToken()
|
||||
showPleaseLoginAgain()
|
||||
const routeName = router.currentRoute.value.name
|
||||
const isPublicRoute =
|
||||
router.currentRoute.value.meta?.public === true ||
|
||||
@@ -113,6 +114,19 @@ _axios.interceptors.response.use(
|
||||
const DEFAULT_LOAD_ERROR = 'خطا در بارگذاری اطلاعات'
|
||||
const SERVER_ERROR = 'خطا در برقراری ارتباط با سرور'
|
||||
const NETWORK_ERROR = 'لطفا اینترنت خود را بررسی کنید و مجددا تلاش کنید'
|
||||
const LOGIN_AGAIN = 'لطفا مجددا وارد شوید'
|
||||
let lastAuthToastAt = 0
|
||||
|
||||
function showPleaseLoginAgain() {
|
||||
const now = Date.now()
|
||||
if (now - lastAuthToastAt < 2500) return
|
||||
lastAuthToastAt = now
|
||||
message.error({
|
||||
content: LOGIN_AGAIN,
|
||||
key: 'please-login-again',
|
||||
duration: 3,
|
||||
})
|
||||
}
|
||||
|
||||
function withHttpStatus(message: string, status: number): string {
|
||||
return `${message} (${status})`
|
||||
|
||||
Reference in New Issue
Block a user