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:
2026-08-26 16:59:29 +03:30
co-authored by Cursor
parent ff3a1cb00b
commit 4675a5b5e3
10 changed files with 128 additions and 17 deletions
+5
View File
@@ -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,
+45 -5
View File
@@ -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
View File
@@ -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()
+14
View File
@@ -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})`