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:
@@ -107,9 +107,20 @@ body {
|
||||
z-index: 5001 !important;
|
||||
}
|
||||
|
||||
//.van-overlay{
|
||||
// z-index: 2000 !important;
|
||||
//}
|
||||
.van-overlay{
|
||||
z-index: 5000 !important;
|
||||
background: rgba(0, 0, 0, 0.55) !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
body.van-overflow-hidden {
|
||||
.bottom-appbar,
|
||||
.app-credit-footer,
|
||||
#support-button {
|
||||
visibility: hidden !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.color-transition {
|
||||
transition: color 0.3s ease;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
</style>-->
|
||||
<template>
|
||||
<VanActionSheet :show="show" @update:show="$emit('update:show', $event)" :placement="placement" teleport="body">
|
||||
<VanActionSheet :show="show" overlay lock-scroll close-on-click-overlay teleport="body" :placement="placement" @update:show="$emit('update:show', $event)">
|
||||
<div class="justify-center cursor-pointer flex mt-1" @click="$emit('update:show', false)">
|
||||
<span class="rounded-sm w-12 border-t-2 border-t-solid border-t-gray-500 mt-2"></span>
|
||||
</div>
|
||||
|
||||
@@ -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})`
|
||||
|
||||
@@ -15,11 +15,7 @@ function goHome() {
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
if (window.history.length > 1) {
|
||||
router.back()
|
||||
return
|
||||
}
|
||||
goHome()
|
||||
router.back()
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -21,6 +21,14 @@ export function resolveAboutSoftwareBody(
|
||||
return { kind: 'text', text: getDefaultAboutSoftwareText(projectName) }
|
||||
}
|
||||
if (looksLikeHtml(value)) {
|
||||
const stripped = value
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/ /gi, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
if (!stripped) {
|
||||
return { kind: 'text', text: getDefaultAboutSoftwareText(projectName) }
|
||||
}
|
||||
return { kind: 'html', html: value }
|
||||
}
|
||||
return { kind: 'text', text: value }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { getConstByKey } from '@/api/const'
|
||||
import { useSettingsStore } from '@/store/settingsStore'
|
||||
import { normalizePublicFileUrl } from '@/utils/productImage'
|
||||
|
||||
function parseJsonArray(raw: unknown): unknown[] {
|
||||
if (raw == null || raw === '') {
|
||||
@@ -69,7 +70,14 @@ async function fetchBanners(force = false) {
|
||||
}
|
||||
}
|
||||
|
||||
banners.value = list.filter((banner) => (banner as { show?: boolean }).show === true)
|
||||
banners.value = list
|
||||
.filter((banner) => (banner as { show?: boolean }).show === true)
|
||||
.map((banner) => {
|
||||
if (!banner || typeof banner !== 'object') return banner
|
||||
const row = banner as { icon?: unknown }
|
||||
if (typeof row.icon !== 'string') return banner
|
||||
return { ...row, icon: normalizePublicFileUrl(row.icon) ?? '' }
|
||||
})
|
||||
bannersLoaded = true
|
||||
} catch (error) {
|
||||
console.error('Error loading banners:', error)
|
||||
|
||||
@@ -6,7 +6,18 @@
|
||||
'dhome-hero-price--boursecu-closed': isBoursecu && displayState !== 'open',
|
||||
}"
|
||||
>
|
||||
<template v-if="displayState !== 'open'">
|
||||
<div
|
||||
v-if="quotePending"
|
||||
class="flex flex-col items-center gap-2 py-4"
|
||||
aria-busy="true"
|
||||
aria-label="در حال بارگذاری قیمت"
|
||||
>
|
||||
<div class="h-3 w-24 rounded bg-gray-200 animate-pulse" />
|
||||
<div class="h-4 w-32 rounded bg-gray-200 animate-pulse" />
|
||||
<div class="h-8 w-40 rounded bg-gray-200 animate-pulse" />
|
||||
</div>
|
||||
|
||||
<template v-else-if="displayState !== 'open'">
|
||||
<div class="dhome-hero-price__closed" :class="{ 'dhome-hero-price__closed--default': !isBoursecu }">
|
||||
<div class="dhome-hero-price__closed-icon">
|
||||
<BasicIcon
|
||||
@@ -61,6 +72,7 @@ import {
|
||||
NO_QUOTE_MESSAGE,
|
||||
getPriceDisplayState,
|
||||
getStoreClosedMessage,
|
||||
isHomeQuotePending,
|
||||
} from '@/utils/storeHours'
|
||||
import { formatSignedPercentChange } from '@/utils/usePrice'
|
||||
|
||||
@@ -69,6 +81,7 @@ const userStore = useUserStore()
|
||||
const isBoursecu = isBoursecuProject()
|
||||
|
||||
const displayState = computed(() => getPriceDisplayState())
|
||||
const quotePending = computed(() => isHomeQuotePending())
|
||||
|
||||
const statusTitle = computed(() =>
|
||||
displayState.value === 'closed' ? 'فروشگاه بسته است' : NO_QUOTE_MESSAGE
|
||||
|
||||
Reference in New Issue
Block a user