Port modular price, product, and PWA fixes into boursecu
Sync live quote handling, SSE product images, store hours, file URL normalization, PWA icon cache, TV monitor, about software, SuperAdmin menu gates, price alerts, and related UX from modular while preserving boursecu branding and config constraints. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,7 +3,18 @@
|
||||
class="w-full relative mt-2 rounded-lg py-3 flex flex-col justify-center home_appear_fade_in transition-colors duration-300"
|
||||
:class="containerClass"
|
||||
>
|
||||
<template v-if="displayState !== 'open'">
|
||||
<div
|
||||
v-if="quotePending"
|
||||
class="flex flex-col items-center gap-2 px-4 py-2"
|
||||
aria-busy="true"
|
||||
aria-label="در حال بارگذاری قیمت"
|
||||
>
|
||||
<div class="h-3 w-32 rounded boursecu-price-skeleton animate-pulse" />
|
||||
<div class="h-6 w-48 rounded boursecu-price-skeleton animate-pulse" />
|
||||
<div class="h-8 w-24 rounded-lg boursecu-price-skeleton animate-pulse" />
|
||||
</div>
|
||||
|
||||
<template v-else-if="displayState !== 'open'">
|
||||
<div class="status-box status-box--boursecu">
|
||||
<div class="status-box__icon-wrap">
|
||||
<Icon
|
||||
@@ -57,12 +68,14 @@ import {
|
||||
STORE_CLOSED_TITLE,
|
||||
getPriceDisplayState,
|
||||
getStoreClosedHint,
|
||||
isHomeQuotePending,
|
||||
} from '@/utils/storeHours'
|
||||
import { formatSignedPercentChange, usePriceWithoutType } from '@/utils/usePrice'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const displayState = computed(() => getPriceDisplayState())
|
||||
const quotePending = computed(() => isHomeQuotePending())
|
||||
|
||||
const statusTitle = computed(() =>
|
||||
displayState.value === 'closed' ? STORE_CLOSED_TITLE : NO_QUOTE_MESSAGE
|
||||
@@ -76,6 +89,9 @@ const statusHint = computed(() => {
|
||||
})
|
||||
|
||||
const containerClass = computed(() => {
|
||||
if (quotePending.value) {
|
||||
return 'boursecu-price-box boursecu-price-box--open'
|
||||
}
|
||||
if (displayState.value !== 'open') {
|
||||
return 'boursecu-price-box boursecu-price-box--closed'
|
||||
}
|
||||
@@ -129,6 +145,10 @@ void price
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.boursecu-price-skeleton {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.status-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<DotLottieVue
|
||||
class="w-11rem h-11rem"
|
||||
autoplay
|
||||
src="https://lottie.host/129cf3a1-df84-4758-a856-58c133ae6013/R5RqclXA8G.json"
|
||||
v-bind="lottieBind"
|
||||
/>
|
||||
<div class="p-5 text-sm">آیا از خروج خود اطمینان دارید؟</div>
|
||||
</div>
|
||||
@@ -23,7 +23,8 @@
|
||||
<script setup lang="ts">
|
||||
import DialogVant from '@/components/DialogVant.vue'
|
||||
import { DotLottieVue } from '@lottiefiles/dotlottie-vue'
|
||||
import {computed} from "vue";
|
||||
import {computed, ref, watch} from 'vue'
|
||||
import {getLottieData, lottieSrc, preloadLottieAssets} from '@/utils/lottieAssets'
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
@@ -34,8 +35,29 @@ const emit = defineEmits<{
|
||||
confirm: []
|
||||
}>()
|
||||
|
||||
const cachedData = ref(getLottieData('logoutConfirm'))
|
||||
|
||||
const lottieBind = computed(() => {
|
||||
if (cachedData.value) return {data: cachedData.value}
|
||||
return {src: lottieSrc('logoutConfirm')}
|
||||
})
|
||||
|
||||
async function ensureLottie() {
|
||||
if (cachedData.value) return
|
||||
await preloadLottieAssets()
|
||||
cachedData.value = getLottieData('logoutConfirm')
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(open) => {
|
||||
if (open) void ensureLottie()
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.show,
|
||||
set: (val) => emit('update:show', val)
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { getConstByKey } from '@/api/const'
|
||||
import { useSettingsStore } from '@/store/settingsStore'
|
||||
import { normalizePublicFileUrl } from '@/utils/file'
|
||||
|
||||
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)
|
||||
|
||||
@@ -2,11 +2,22 @@
|
||||
<div
|
||||
class="dhome-surface dhome-hero-price"
|
||||
:class="{
|
||||
'dhome-hero-price--boursecu-open': displayState === 'open',
|
||||
'dhome-hero-price--boursecu-closed': displayState !== 'open',
|
||||
'dhome-hero-price--boursecu-open': displayState === 'open' || quotePending,
|
||||
'dhome-hero-price--boursecu-closed': displayState !== 'open' && !quotePending,
|
||||
}"
|
||||
>
|
||||
<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 boursecu-price-skeleton animate-pulse" />
|
||||
<div class="h-4 w-32 rounded boursecu-price-skeleton animate-pulse" />
|
||||
<div class="h-8 w-40 rounded boursecu-price-skeleton animate-pulse" />
|
||||
</div>
|
||||
|
||||
<template v-else-if="displayState !== 'open'">
|
||||
<div class="dhome-hero-price__closed">
|
||||
<div class="dhome-hero-price__closed-icon">
|
||||
<BasicIcon
|
||||
@@ -56,12 +67,14 @@ import {
|
||||
STORE_CLOSED_TITLE,
|
||||
getPriceDisplayState,
|
||||
getStoreClosedHint,
|
||||
isHomeQuotePending,
|
||||
} from '@/utils/storeHours'
|
||||
import { formatSignedPercentChange, usePriceWithoutType } from '@/utils/usePrice'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const displayState = computed(() => getPriceDisplayState())
|
||||
const quotePending = computed(() => isHomeQuotePending())
|
||||
|
||||
const statusTitle = computed(() =>
|
||||
displayState.value === 'closed' ? STORE_CLOSED_TITLE : NO_QUOTE_MESSAGE
|
||||
@@ -110,6 +123,10 @@ const percentChange = computed(() => {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.boursecu-price-skeleton {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.dhome-hero-price__closed {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -36,7 +36,13 @@ const resolvedActions = computed(() => {
|
||||
})
|
||||
|
||||
function handleClick(action: HomeQuickActionItem) {
|
||||
router.push(action.route)
|
||||
if (action.handler) {
|
||||
action.handler()
|
||||
return
|
||||
}
|
||||
if (action.route) {
|
||||
router.push(action.route)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -38,7 +38,13 @@ const resolvedActions = computed(() => {
|
||||
})
|
||||
|
||||
function navigate(action: HomeQuickActionItem) {
|
||||
router.push(action.route)
|
||||
if (action.handler) {
|
||||
action.handler()
|
||||
return
|
||||
}
|
||||
if (action.route) {
|
||||
router.push(action.route)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
class="w-5rem h-5rem mt-[-1.6rem] ml-[-2rem]"
|
||||
autoplay
|
||||
loop
|
||||
src="https://lottie.host/3d46ebb3-a580-41eb-ade7-b8829c32b071/PJPuSKbpze.json"
|
||||
src="/lottie/goldBars.json"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -315,7 +315,7 @@
|
||||
<DotLottieVue
|
||||
class="w-11rem h-11rem"
|
||||
autoplay
|
||||
src="https://lottie.host/129cf3a1-df84-4758-a856-58c133ae6013/R5RqclXA8G.json"
|
||||
src="/lottie/logoutConfirm.json"
|
||||
/>
|
||||
<div class="p-5 text-sm">آیا از خروج خود اطمینان دارید؟</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getBoursecuActionIconSrc } from '@/utils/config'
|
||||
import { openGoldDayCalculator } from '@/composables/useGoldDayCalculator'
|
||||
import type { HomeQuickActionItem } from '../types'
|
||||
|
||||
export const boursecuQuickActions: HomeQuickActionItem[] = [
|
||||
@@ -44,4 +45,10 @@ export const boursecuQuickActions: HomeQuickActionItem[] = [
|
||||
iconSrc: getBoursecuActionIconSrc('convert'),
|
||||
route: { name: 'buyProduct' },
|
||||
},
|
||||
{
|
||||
id: 'calculator',
|
||||
label: 'محاسبهگر',
|
||||
icon: 'vuesax-linear:chart-2',
|
||||
handler: () => openGoldDayCalculator(),
|
||||
},
|
||||
]
|
||||
@@ -35,7 +35,13 @@ const resolvedActions = computed(() => {
|
||||
})
|
||||
|
||||
function handleClick(action: HomeQuickActionItem) {
|
||||
router.push(action.route)
|
||||
if (action.handler) {
|
||||
action.handler()
|
||||
return
|
||||
}
|
||||
if (action.route) {
|
||||
router.push(action.route)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@ export interface HomeQuickActionItem {
|
||||
label: string
|
||||
icon: string
|
||||
iconSrc?: string
|
||||
route: { name: string; query?: Record<string, string> }
|
||||
route?: { name: string; query?: Record<string, string> }
|
||||
handler?: () => void
|
||||
}
|
||||
|
||||
export interface HomeLayoutSectionConfig {
|
||||
|
||||
Reference in New Issue
Block a user