fix some bugs from ghatreh
This commit is contained in:
@@ -12,7 +12,7 @@ import {
|
||||
setFingerTimeOut,
|
||||
setHasFingerPrint
|
||||
} from '@/app/authBoot'
|
||||
import { recoverMobileViewportAfterAuth } from '@/layouts/composables/useMobileScrollReset'
|
||||
import { clearVantOverlays, recoverMobileViewportAfterAuth } from '@/layouts/composables/useMobileScrollReset'
|
||||
|
||||
export function setupRouterGuards() {
|
||||
const userStore = useUserStore()
|
||||
@@ -180,6 +180,9 @@ export function setupRouterGuards() {
|
||||
})
|
||||
|
||||
router.afterEach((to, from) => {
|
||||
if (to.name === 'login') {
|
||||
clearVantOverlays()
|
||||
}
|
||||
if (document.documentElement.classList.contains('app-layout--desktop')) return
|
||||
if (from.name === 'login') {
|
||||
recoverMobileViewportAfterAuth()
|
||||
|
||||
@@ -120,6 +120,10 @@ body {
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
.van-dialog {
|
||||
z-index: var(--app-z-dialog, 5300) !important;
|
||||
}
|
||||
|
||||
body.van-overflow-hidden {
|
||||
.bottom-appbar,
|
||||
.app-credit-footer,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ref } from 'vue'
|
||||
import { getProducts } from '@/api/products'
|
||||
import type { Product } from '@/models/products'
|
||||
import { getLastStreamProductsList } from '@/utils/productStreamBridge'
|
||||
|
||||
/** کش مشترک لیست محصولات — dedupe درخواستهای polling */
|
||||
const catalogProducts = ref<Product[]>([])
|
||||
@@ -12,22 +11,11 @@ export function getProductsCatalogRef() {
|
||||
}
|
||||
|
||||
export function findProductInCatalog(id: number): Product | undefined {
|
||||
const cached = catalogProducts.value.find((p) => p.id === id)
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
|
||||
return getLastStreamProductsList()?.find((p) => p.id === id)
|
||||
return catalogProducts.value.find((p) => p.id === id)
|
||||
}
|
||||
|
||||
export function getProductsCatalog(force = false): Promise<Product[]> {
|
||||
if (!force) {
|
||||
const streamed = getLastStreamProductsList()
|
||||
if (streamed?.length) {
|
||||
catalogProducts.value = streamed
|
||||
return Promise.resolve(streamed)
|
||||
}
|
||||
|
||||
if (catalogProducts.value.length > 0) {
|
||||
return Promise.resolve(catalogProducts.value)
|
||||
}
|
||||
|
||||
@@ -41,6 +41,12 @@ function clearBodyScrollLock() {
|
||||
}
|
||||
}
|
||||
|
||||
/** leftover van-dialog overlay after logout/navigation — blocks all clicks */
|
||||
export function clearVantOverlays() {
|
||||
document.querySelectorAll('body > .van-overlay').forEach((el) => el.remove())
|
||||
clearBodyScrollLock()
|
||||
}
|
||||
|
||||
function resetAllScrollPositions() {
|
||||
window.scrollTo(0, 0)
|
||||
document.documentElement.scrollTop = 0
|
||||
|
||||
@@ -47,6 +47,7 @@ const {
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
() => {
|
||||
closeOverflowMenu()
|
||||
resetMobileAppScroll()
|
||||
},
|
||||
{ flush: 'post' },
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
<Transition>
|
||||
<div
|
||||
v-if="show"
|
||||
class="top-0 bottom-0 bg-black wrap-fix z-5000 opacity-60"
|
||||
class="top-0 bottom-0 bg-black wrap-fix z-5200 opacity-60"
|
||||
@click="$emit('close')"
|
||||
/>
|
||||
</Transition>
|
||||
<Transition name="slide-fade">
|
||||
<div v-if="show" class="wrap-fix bottom-11vh z-5000 flex justify-center items-end">
|
||||
<div
|
||||
v-if="show"
|
||||
class="wrap-fix z-5200 flex justify-center items-end bottom-[calc(var(--app-bottom-chrome-height)+1.5rem)]"
|
||||
>
|
||||
<div class="relative flex flex-col items-center">
|
||||
<div
|
||||
class="flex items-center justify-center bg-white rounded-full w-[30px] h-[30px] mb-2"
|
||||
|
||||
@@ -53,6 +53,9 @@
|
||||
&__title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__trailing {
|
||||
|
||||
@@ -234,6 +234,11 @@ export function getProductSellPrice(product?: Product | null): number {
|
||||
return base - resolveProductCashFeeSellRial(product, base)
|
||||
}
|
||||
|
||||
/** کارت فروشگاه: قیمت نمایشی صفر → «بدون قیمت» */
|
||||
export function isProductWithoutDisplayPrice(product?: Product | null): boolean {
|
||||
return getProductBuyPrice(product) <= 0
|
||||
}
|
||||
|
||||
export function isProductPriceInactive(product?: Product | null): boolean {
|
||||
if (!product) {
|
||||
return true
|
||||
|
||||
@@ -9,12 +9,14 @@ function asBool(value: boolean | number | null | undefined) {
|
||||
|
||||
export function isProductBuyOpen(product: TradeOpenProduct) {
|
||||
if (!product) return true
|
||||
return asBool(product?.open_buy)
|
||||
if (product.open_buy === undefined || product.open_buy === null) return true
|
||||
return asBool(product.open_buy)
|
||||
}
|
||||
|
||||
export function isProductSellOpen(product: TradeOpenProduct) {
|
||||
if (!product) return true
|
||||
return asBool(product?.open_sell)
|
||||
if (product.open_sell === undefined || product.open_sell === null) return true
|
||||
return asBool(product.open_sell)
|
||||
}
|
||||
|
||||
export function isAnyProductTradeOpen(product: TradeOpenProduct) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<DialogVant v-model:show="visible" :closeOnClickOverlay="false" :timeOut="-1">
|
||||
<DialogVant v-model:show="visible" :closeOnClickOverlay="true" :timeOut="-1">
|
||||
<template v-slot:title>
|
||||
<div class="flex flex-col items-center">
|
||||
<DotLottieVue
|
||||
|
||||
@@ -337,7 +337,7 @@
|
||||
</div>
|
||||
<AppHeader @logout="showLogoutConfirm = true" />
|
||||
</div>
|
||||
<DialogVant v-model:show="showLogoutConfirm" :closeOnClickOverlay="false" :timeOut="-1">
|
||||
<DialogVant v-model:show="showLogoutConfirm" :closeOnClickOverlay="true" :timeOut="-1">
|
||||
<!-- <template v-slot:img>
|
||||
<img :src="registerDelayImage" alt="" class="opacity-80">
|
||||
</template>-->
|
||||
@@ -411,7 +411,7 @@ import 'swiper/swiper-bundle.css';
|
||||
import img2 from '@/assets/img/banner/2.png'
|
||||
import Icon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { usePrice, usePriceWithoutType, useTowDecimal, formatGoldAmount } from '../../../utils/usePrice'
|
||||
import {computed, createVNode, onActivated, onMounted, onUnmounted, ref, watch} from 'vue'
|
||||
import {computed, createVNode, nextTick, onActivated, onMounted, onUnmounted, ref, watch} from 'vue'
|
||||
import router from '@/router'
|
||||
import * as echarts from 'echarts'
|
||||
import { useIntersectionObserver } from '@vueuse/core'
|
||||
@@ -443,6 +443,7 @@ import type { PInventory, Product } from '@/models/products'
|
||||
import { useWalletAssets } from '@/composables/useWalletAssets'
|
||||
import { resolveAssetsRialValue } from '@/utils/productAssets'
|
||||
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||
import { clearVantOverlays } from '@/layouts/composables/useMobileScrollReset'
|
||||
import { message, Modal, Divider } from 'ant-design-vue'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import BasicModal from '@/components/BasicPacks/BasicModal.vue'
|
||||
@@ -864,17 +865,13 @@ const showLogoutConfirm = ref<boolean>()
|
||||
|
||||
async function logout() {
|
||||
logoutLoading.value = true
|
||||
// baseConfirm('آیا از حساب کاربری خارج می شوید؟',async () => {
|
||||
showLogoutConfirm.value = false
|
||||
userStore.userInfo = undefined
|
||||
walletStore.walletInfo = undefined
|
||||
/* userStore.$reset()
|
||||
walletStore.$reset()*/
|
||||
removeToken()
|
||||
await router.push({ name: 'login' })
|
||||
logoutLoading.value = true
|
||||
window.location.reload()
|
||||
// console.log('salam')
|
||||
// })
|
||||
await nextTick()
|
||||
clearVantOverlays()
|
||||
window.location.assign(router.resolve({ name: 'login' }).href)
|
||||
}
|
||||
|
||||
async function openGoldModal() {
|
||||
|
||||
@@ -64,8 +64,10 @@
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="product-card relative flex cursor-pointer rounded-xl border border-gray-200 bg-white p-3 transition-all duration-300 hover:-translate-y-0.5 hover:shadow-lg"
|
||||
:class="!isProductTradingHoursOpen(props.ProductItem) && 'opacity-50 grayscale'"
|
||||
class="product-card relative flex rounded-xl border border-gray-200 bg-white p-3 transition-all duration-300"
|
||||
:class="isDimmed
|
||||
? 'cursor-not-allowed opacity-50 grayscale pointer-events-none'
|
||||
: 'cursor-pointer hover:-translate-y-0.5 hover:shadow-lg'"
|
||||
>
|
||||
<span
|
||||
v-if="isOutOfStock"
|
||||
@@ -93,8 +95,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 !text-primary text-sm font-bold">
|
||||
{{ usePrice(getProductBuyPrice(props.ProductItem)) }}
|
||||
<div
|
||||
class="mt-2 text-sm font-bold"
|
||||
:class="hasNoPrice ? 'text-gray-400' : '!text-primary'"
|
||||
>
|
||||
{{ hasNoPrice ? 'بدون قیمت' : usePrice(getProductBuyPrice(props.ProductItem)) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -104,7 +109,7 @@
|
||||
import { computed } from 'vue'
|
||||
import type { Product } from '@/models/products'
|
||||
import { usePrice } from '@/utils/usePrice'
|
||||
import { getProductBuyPrice } from '@/utils/productPrice'
|
||||
import { getProductBuyPrice, isProductWithoutDisplayPrice } from '@/utils/productPrice'
|
||||
import { isProductOutOfStock } from '@/utils/productCategories'
|
||||
import { isProductTradingHoursOpen } from '@/utils/productTradeOpen'
|
||||
import ProductImage from '@/components/ProductImage.vue'
|
||||
@@ -116,6 +121,10 @@ interface Props {
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const isOutOfStock = computed(() => isProductOutOfStock(props.ProductItem))
|
||||
const hasNoPrice = computed(() => isProductWithoutDisplayPrice(props.ProductItem))
|
||||
const isDimmed = computed(
|
||||
() => hasNoPrice.value || !isProductTradingHoursOpen(props.ProductItem),
|
||||
)
|
||||
|
||||
const amountProduct = computed(() => {
|
||||
if (!props.ProductItem.amount) return ''
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<template>
|
||||
<div
|
||||
class="product-card relative flex h-full flex-col rounded-xl border border-gray-200 bg-white p-3 transition-all duration-300 hover:-translate-y-1 hover:shadow-lg cursor-pointer"
|
||||
:class="!isProductTradingHoursOpen(props.ProductItem) && 'opacity-50 grayscale'"
|
||||
class="product-card relative flex h-full flex-col rounded-xl border border-gray-200 bg-white p-3 transition-all duration-300"
|
||||
:class="isDimmed
|
||||
? 'cursor-not-allowed opacity-50 grayscale pointer-events-none'
|
||||
: 'cursor-pointer hover:-translate-y-1 hover:shadow-lg'"
|
||||
>
|
||||
<span
|
||||
v-if="isOutOfStock"
|
||||
@@ -26,8 +28,11 @@
|
||||
{{ amountProduct }}
|
||||
</div>
|
||||
|
||||
<div class="mt-auto !text-primary text-sm font-bold">
|
||||
{{ usePrice(getProductBuyPrice(props.ProductItem)) }}
|
||||
<div
|
||||
class="mt-auto text-sm font-bold"
|
||||
:class="hasNoPrice ? 'text-gray-400' : '!text-primary'"
|
||||
>
|
||||
{{ hasNoPrice ? 'بدون قیمت' : usePrice(getProductBuyPrice(props.ProductItem)) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -36,7 +41,7 @@
|
||||
import { computed } from 'vue'
|
||||
import type { Product } from '@/models/products'
|
||||
import { usePrice } from '@/utils/usePrice'
|
||||
import { getProductBuyPrice } from '@/utils/productPrice'
|
||||
import { getProductBuyPrice, isProductWithoutDisplayPrice } from '@/utils/productPrice'
|
||||
import { isProductOutOfStock } from '@/utils/productCategories'
|
||||
import { isProductTradingHoursOpen } from '@/utils/productTradeOpen'
|
||||
import ProductImage from '@/components/ProductImage.vue'
|
||||
@@ -48,6 +53,10 @@ interface Props {
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const isOutOfStock = computed(() => isProductOutOfStock(props.ProductItem))
|
||||
const hasNoPrice = computed(() => isProductWithoutDisplayPrice(props.ProductItem))
|
||||
const isDimmed = computed(
|
||||
() => hasNoPrice.value || !isProductTradingHoursOpen(props.ProductItem),
|
||||
)
|
||||
|
||||
const amountProduct = computed(() => {
|
||||
if (!props.ProductItem.amount) return ''
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
priceWssEnabled,
|
||||
shouldUsePricePolling,
|
||||
} from '@/utils/priceSocketState'
|
||||
import { subscribeProductListStream } from '@/utils/productStreamBridge'
|
||||
import { getLastStreamProductsList, subscribeProductListStream } from '@/utils/productStreamBridge'
|
||||
|
||||
let initPromise: Promise<void> | null = null
|
||||
let productListInterval: ReturnType<typeof setInterval> | undefined
|
||||
@@ -96,9 +96,15 @@ function mergeLiveProductList(incoming: Product[], current?: Product[]): Product
|
||||
const prevId = productFileId(prev)
|
||||
const nextId = productFileId(item)
|
||||
|
||||
const keepTradeFlags = {
|
||||
open_buy: 'open_buy' in item ? item.open_buy : prev.open_buy,
|
||||
open_sell: 'open_sell' in item ? item.open_sell : prev.open_sell,
|
||||
}
|
||||
|
||||
if (!productHasDisplayImage(item) && productHasDisplayImage(prev)) {
|
||||
return {
|
||||
...item,
|
||||
...keepTradeFlags,
|
||||
file: prev.file,
|
||||
file_id: prev.file_id ?? (prev.file as { id?: number } | undefined)?.id ?? item.file_id,
|
||||
img: prev.img,
|
||||
@@ -108,13 +114,14 @@ function mergeLiveProductList(incoming: Product[], current?: Product[]): Product
|
||||
if (prevId > nextId) {
|
||||
return {
|
||||
...item,
|
||||
...keepTradeFlags,
|
||||
file: prev.file,
|
||||
file_id: prev.file_id,
|
||||
img: prev.img,
|
||||
}
|
||||
}
|
||||
|
||||
return item
|
||||
return { ...item, ...keepTradeFlags }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -302,11 +309,9 @@ export function onProductsPageProviderUnmounted() {
|
||||
}
|
||||
|
||||
export async function initProductsPageData(force = false) {
|
||||
if (!force && initialized.value) {
|
||||
if (!force && initialized.value && productsFirstItems.value?.length) {
|
||||
syncProductListPolling()
|
||||
if (productsFirstItems.value?.length) {
|
||||
productsPageLoading.value = false
|
||||
}
|
||||
productsPageLoading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
@@ -320,10 +325,13 @@ export async function initProductsPageData(force = false) {
|
||||
productsPageLoading.value = true
|
||||
}
|
||||
await loadCategoryTabs()
|
||||
await refreshProductsPageData()
|
||||
|
||||
// stream-first: polling فقط وقتی SSE قطع/غیرفعال است
|
||||
if (shouldUsePricePolling()) {
|
||||
await refreshProductsPageData()
|
||||
if (!productsFirstItems.value?.length) {
|
||||
const streamed = getLastStreamProductsList()
|
||||
if (streamed?.length) {
|
||||
applyProductsList(streamed)
|
||||
}
|
||||
}
|
||||
|
||||
initialized.value = true
|
||||
@@ -331,11 +339,7 @@ export async function initProductsPageData(force = false) {
|
||||
} catch (error) {
|
||||
console.error('Products page initialization error:', error)
|
||||
} finally {
|
||||
// اگر استریم قبل از init لیست را داده، loading را دوباره true نکردهایم؛
|
||||
// اگر API/polling تمام شد یا محصول داریم، اسپینر را قطع کن.
|
||||
if (productsFirstItems.value?.length || shouldUsePricePolling()) {
|
||||
productsPageLoading.value = false
|
||||
}
|
||||
productsPageLoading.value = false
|
||||
initPromise = null
|
||||
}
|
||||
})()
|
||||
|
||||
@@ -79,7 +79,7 @@ import img from '@/assets/img/user_ly1.svg'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import ProductItemVertical from '@/views/base/products/components/ProductItemVertical.vue'
|
||||
import MobileProductsView from '@/views/base/products/mobile/MobileProductsView.vue'
|
||||
import { isProductVisible } from '@/utils/productPrice'
|
||||
import { isProductVisible, isProductWithoutDisplayPrice } from '@/utils/productPrice'
|
||||
import { isProductTradingHoursOpen } from '@/utils/productTradeOpen'
|
||||
import {
|
||||
productsActiveKey,
|
||||
@@ -105,6 +105,7 @@ const visibleTypeItems = computed(() => {
|
||||
|
||||
function clickProduct(item: Product) {
|
||||
if (!isProductTradingHoursOpen(item)) return
|
||||
if (isProductWithoutDisplayPrice(item)) return
|
||||
if (isProductVisible(item)) {
|
||||
router.push({ name: 'productDetail', params: { id: item.id } })
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { findProductInCatalog, getProductsCatalog, getProductsCatalogRef } from
|
||||
import { getLastPrices, stopPriceInterval } from '@/utils/price'
|
||||
import { getProductBuyPrice } from '@/utils/productPrice'
|
||||
import { isProductOutOfStock } from '@/utils/productCategories'
|
||||
import { isProductTradingHoursOpen } from '@/utils/productTradeOpen'
|
||||
import { isProductBuyOpen, isProductTradingHoursOpen } from '@/utils/productTradeOpen'
|
||||
import { loading } from '@/utils'
|
||||
import type { Product } from '@/models/products'
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface ProductDetailPageContext {
|
||||
showFilter: ReturnType<typeof ref<boolean>>
|
||||
isOutOfStock: ReturnType<typeof computed<boolean>>
|
||||
hoursOpen: ReturnType<typeof computed<boolean>>
|
||||
canBuy: ReturnType<typeof computed<boolean>>
|
||||
handleBuyClick: () => void
|
||||
goToProduct: (id: number) => void
|
||||
getProductBuyPrice: typeof getProductBuyPrice
|
||||
@@ -37,6 +38,7 @@ export function useProductDetailPage() {
|
||||
|
||||
const isOutOfStock = computed(() => isProductOutOfStock(product.value))
|
||||
const hoursOpen = computed(() => isProductTradingHoursOpen(product.value))
|
||||
const canBuy = computed(() => isProductBuyOpen(product.value))
|
||||
|
||||
watch(productId, (newId, oldId) => {
|
||||
if (newId && newId !== oldId) {
|
||||
@@ -46,7 +48,7 @@ export function useProductDetailPage() {
|
||||
})
|
||||
|
||||
watch(
|
||||
() => getProductsCatalogRef().value.map((p) => `${p.id}:${p.trade_open}:${p.show}`).join('|'),
|
||||
() => getProductsCatalogRef().value.map((p) => `${p.id}:${p.trade_open}:${p.show}:${p.open_buy}`).join('|'),
|
||||
() => {
|
||||
const current = product.value
|
||||
if (!current?.id) return
|
||||
@@ -56,6 +58,12 @@ export function useProductDetailPage() {
|
||||
if (typeof live.trade_open === 'boolean') {
|
||||
current.trade_open = live.trade_open
|
||||
}
|
||||
if (typeof live.open_buy === 'boolean') {
|
||||
current.open_buy = live.open_buy
|
||||
}
|
||||
if (typeof live.open_sell === 'boolean') {
|
||||
current.open_sell = live.open_sell
|
||||
}
|
||||
if (showFilter.value && !isProductTradingHoursOpen(current)) {
|
||||
showFilter.value = false
|
||||
}
|
||||
@@ -65,9 +73,16 @@ export function useProductDetailPage() {
|
||||
async function loadProduct() {
|
||||
try {
|
||||
loading.value = true
|
||||
await getProductsCatalog()
|
||||
let foundProduct = findProductInCatalog(Number(productId.value))
|
||||
if (!foundProduct) foundProduct = await getProductById(Number(productId.value))
|
||||
const id = Number(productId.value)
|
||||
const [detail] = await Promise.all([
|
||||
getProductById(id).catch(() => null),
|
||||
getProductsCatalog(),
|
||||
])
|
||||
const fromCatalog = findProductInCatalog(id)
|
||||
const foundProduct =
|
||||
detail && typeof detail === 'object'
|
||||
? { ...fromCatalog, ...detail }
|
||||
: fromCatalog
|
||||
if (!foundProduct) {
|
||||
message.error('محصول یافت نشد')
|
||||
await router.push({ name: 'buyProduct' })
|
||||
@@ -110,6 +125,7 @@ export function useProductDetailPage() {
|
||||
showFilter,
|
||||
isOutOfStock,
|
||||
hoursOpen,
|
||||
canBuy,
|
||||
handleBuyClick,
|
||||
goToProduct,
|
||||
getProductBuyPrice,
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<div class="dproduct-info__price-row">
|
||||
<span class="dproduct-info__price">{{ usePrice(getProductBuyPrice(product)) }}</span>
|
||||
<BasicButton
|
||||
v-if="hoursOpen && product.open_buy && !isOutOfStock"
|
||||
v-if="hoursOpen && canBuy && !isOutOfStock"
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="handleBuyClick"
|
||||
@@ -26,7 +26,7 @@
|
||||
خرید محصول
|
||||
</BasicButton>
|
||||
<div v-else-if="!hoursOpen" class="text-gray-500">این محصول در حال حاضر در دسترس نیست</div>
|
||||
<div v-else-if="!product.open_buy" class="text-gray-500">خرید این محصول در حال حاضر امکان پذیر نیست</div>
|
||||
<div v-else-if="!canBuy" class="text-gray-500">خرید این محصول در حال حاضر امکان پذیر نیست</div>
|
||||
<div v-else class="text-gray-500">ناموجود</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -73,7 +73,7 @@ import { usePrice } from '@/utils/usePrice'
|
||||
import { isProductOutOfStock } from '@/utils/productCategories'
|
||||
import { useProductDetailPageContext } from '../composables/useProductDetailPage'
|
||||
|
||||
const { product, isOutOfStock, hoursOpen, handleBuyClick, goToProduct, getProductBuyPrice } = useProductDetailPageContext()
|
||||
const { product, isOutOfStock, hoursOpen, canBuy, handleBuyClick, goToProduct, getProductBuyPrice } = useProductDetailPageContext()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,60 +1,88 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="bottom-0 fixed top-0">
|
||||
<img :src="img" alt="" class="w-inherit" :style="{ backgroundRepeat: 'repeat' }" />
|
||||
<div class="relative">
|
||||
<div class="mobile-page-bg" aria-hidden="true">
|
||||
<img :src="img" alt="" />
|
||||
</div>
|
||||
|
||||
<MobilePageHeader :title="product?.title || 'جزئیات محصول'" show-back />
|
||||
|
||||
<a-spin :spinning="loading">
|
||||
<div class="!h-full overflow-y-scroll">
|
||||
<div class="mx-5 pb-30 pt-5" style="color: #1b2b07">
|
||||
<div class="bg-gray-100 rounded-lg w-full flex justify-center mb-4">
|
||||
<ProductImage :product="product" :dimmed="isOutOfStock" img-class="max-w-full h-auto" />
|
||||
<div class="mpage-content relative z-1 pb-[calc(var(--app-bottom-chrome-height)+1rem)]">
|
||||
<div class="flex flex-col gap-3 px-5">
|
||||
<div class="flex justify-center rounded-2xl border border-gray-200 bg-white p-5">
|
||||
<ProductImage
|
||||
:product="product"
|
||||
:dimmed="isOutOfStock"
|
||||
img-class="max-h-52 w-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="text-2xl font-bold mb-4">{{ product?.title }}</div>
|
||||
<div class="flex items-center justify-between mb-4 gap-4">
|
||||
<div class="text-xl font-semibold text-primary">
|
||||
{{ usePrice(getProductBuyPrice(product)) }}
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<BasicButton
|
||||
v-if="hoursOpen && product?.open_buy && !isOutOfStock"
|
||||
class="w-full py-3"
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="handleBuyClick"
|
||||
<div class="flex flex-col gap-3 rounded-2xl border border-gray-200 bg-white p-4">
|
||||
<div>
|
||||
<div class="mb-1 text-xs text-gray-400">قیمت خرید</div>
|
||||
<div
|
||||
class="text-xl font-bold"
|
||||
:class="hasNoPrice ? 'text-gray-400' : 'text-primary'"
|
||||
>
|
||||
خرید محصول
|
||||
</BasicButton>
|
||||
<div v-else-if="!hoursOpen" class="text-center text-gray-500 py-4">
|
||||
این محصول در حال حاضر در دسترس نیست
|
||||
{{ hasNoPrice ? 'بدون قیمت' : usePrice(getProductBuyPrice(product)) }}
|
||||
</div>
|
||||
<div v-else-if="!product?.open_buy" class="text-center text-gray-500 py-4">
|
||||
خرید این محصول در حال حاضر امکان پذیر نیست
|
||||
</div>
|
||||
<div v-else class="text-center text-gray-500 py-4">ناموجود</div>
|
||||
<div v-if="amountLabel" class="mt-1 text-xs text-gray-500">{{ amountLabel }}</div>
|
||||
</div>
|
||||
|
||||
<BasicButton
|
||||
v-if="hoursOpen && canBuy && !isOutOfStock && !hasNoPrice"
|
||||
class="!w-full py-3"
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="handleBuyClick"
|
||||
>
|
||||
خرید محصول
|
||||
</BasicButton>
|
||||
<div
|
||||
v-else
|
||||
class="rounded-xl bg-gray-50 px-3 py-3 text-center text-sm text-gray-500"
|
||||
>
|
||||
{{ statusText }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="product?.description" class="mb-6">
|
||||
<div class="text-lg font-semibold mb-2">توضیحات محصول:</div>
|
||||
<div class="text-base text-gray-700 whitespace-pre-line">{{ product.description }}</div>
|
||||
<div
|
||||
v-if="product?.description"
|
||||
class="flex flex-col gap-1.5 rounded-2xl border border-gray-200 bg-white p-4"
|
||||
>
|
||||
<div class="font-semibold">توضیحات محصول</div>
|
||||
<div class="text-sm text-gray-600 whitespace-pre-line">{{ product.description }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="product?.similar?.length" class="mt-8">
|
||||
<div class="text-lg font-semibold mb-4">محصولات مشابه</div>
|
||||
<swiper :slides-per-view="2.25" :space-between="16" class="similar-products-swiper">
|
||||
<div v-if="product?.similar?.length" class="pt-1">
|
||||
<div class="font-semibold">محصولات مشابه</div>
|
||||
<div class="mb-3 text-xs text-gray-400">از همین دسته میتوانید انتخاب کنید</div>
|
||||
<swiper :slides-per-view="2.15" :space-between="12" class="similar-products-swiper">
|
||||
<swiper-slide v-for="item in product.similar" :key="item.id">
|
||||
<div class="bg-white rounded-lg shadow p-4 cursor-pointer h-full" @click="goToProduct(item.id)">
|
||||
<div class="bg-gray-100 rounded-md flex justify-center p-4 mb-2">
|
||||
<div
|
||||
class="flex h-full flex-col rounded-2xl border border-gray-200 bg-white p-3"
|
||||
:class="isProductWithoutDisplayPrice(item)
|
||||
? 'pointer-events-none cursor-not-allowed opacity-50 grayscale'
|
||||
: 'cursor-pointer'"
|
||||
@click="selectSimilar(item)"
|
||||
>
|
||||
<div class="mb-2 flex h-20 items-center justify-center rounded-xl bg-gray-50">
|
||||
<ProductImage
|
||||
:product="item"
|
||||
:dimmed="isProductOutOfStock(item)"
|
||||
img-class="h-20 object-contain"
|
||||
img-class="h-16 object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-sm font-semibold mb-1 line-clamp-2">{{ item.title }}</div>
|
||||
<div class="text-sm text-primary font-bold mb-3">
|
||||
{{ usePrice(getProductBuyPrice(item)) }}
|
||||
<div class="mb-1 line-clamp-2 min-h-8 text-xs font-semibold">{{ item.title }}</div>
|
||||
<div
|
||||
class="mt-auto text-sm font-bold"
|
||||
:class="isProductWithoutDisplayPrice(item) ? 'text-gray-400' : 'text-primary'"
|
||||
>
|
||||
{{
|
||||
isProductWithoutDisplayPrice(item)
|
||||
? 'بدون قیمت'
|
||||
: usePrice(getProductBuyPrice(item))
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</swiper-slide>
|
||||
@@ -67,17 +95,49 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import img from '@/assets/img/user_ly1.svg'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import ProductImage from '@/components/ProductImage.vue'
|
||||
import MobilePageHeader from '@/layouts/components/MobilePageHeader.vue'
|
||||
import type { Product } from '@/models/products'
|
||||
import { loading } from '@/utils'
|
||||
import { isProductOutOfStock } from '@/utils/productCategories'
|
||||
import { getProductBuyPrice, isProductWithoutDisplayPrice } from '@/utils/productPrice'
|
||||
import { usePrice } from '@/utils/usePrice'
|
||||
import { Swiper, SwiperSlide } from 'swiper/vue'
|
||||
import 'swiper/swiper-bundle.css'
|
||||
import { useProductDetailPageContext } from '../composables/useProductDetailPage'
|
||||
|
||||
const { product, isOutOfStock, hoursOpen, handleBuyClick, goToProduct, getProductBuyPrice } = useProductDetailPageContext()
|
||||
const {
|
||||
product,
|
||||
isOutOfStock,
|
||||
hoursOpen,
|
||||
canBuy,
|
||||
handleBuyClick,
|
||||
goToProduct,
|
||||
} = useProductDetailPageContext()
|
||||
|
||||
const hasNoPrice = computed(() => isProductWithoutDisplayPrice(product.value))
|
||||
|
||||
const amountLabel = computed(() => {
|
||||
const amount = Number(product.value?.amount ?? 0)
|
||||
if (!amount) return ''
|
||||
if (amount < 1) return `${amount * 1000} سوت`
|
||||
return `${amount} گرم`
|
||||
})
|
||||
|
||||
const statusText = computed(() => {
|
||||
if (hasNoPrice.value) return 'قیمت این محصول الان مشخص نیست'
|
||||
if (!hoursOpen.value) return 'این محصول در حال حاضر در دسترس نیست'
|
||||
if (!canBuy.value) return 'خرید این محصول در حال حاضر امکان پذیر نیست'
|
||||
return 'ناموجود'
|
||||
})
|
||||
|
||||
function selectSimilar(item: Product) {
|
||||
if (isProductWithoutDisplayPrice(item)) return
|
||||
goToProduct(item.id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -85,7 +145,7 @@ const { product, isOutOfStock, hoursOpen, handleBuyClick, goToProduct, getProduc
|
||||
color: #d3a233;
|
||||
}
|
||||
.similar-products-swiper {
|
||||
padding-bottom: 10px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
.similar-products-swiper :deep(.swiper-slide) {
|
||||
height: auto;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { computed, inject, onMounted, provide, ref, watch, type InjectionKey } from 'vue'
|
||||
import { computed, inject, nextTick, onMounted, provide, ref, watch, type InjectionKey } from 'vue'
|
||||
import { loading, removeToken } from '@/utils'
|
||||
import {
|
||||
guideTypePassword,
|
||||
@@ -18,6 +18,7 @@ import { useSettingsStore } from '@/store/settingsStore'
|
||||
import { useSupportMessenger } from '@/composables/useSupportMessenger'
|
||||
import { isUserSupportChatEnabled, unreadSupportCount } from '@/utils/chat'
|
||||
import { openGoldDayCalculator } from '@/composables/useGoldDayCalculator'
|
||||
import { clearVantOverlays } from '@/layouts/composables/useMobileScrollReset'
|
||||
|
||||
export interface UserMenuAction {
|
||||
id: string
|
||||
@@ -265,8 +266,9 @@ kh.kashkolgold.ir/login?id=${userStore.userInfo?.referral_id}`
|
||||
walletStore.walletInfo = undefined
|
||||
removeToken()
|
||||
showLogoutConfirm.value = false
|
||||
await router.push({ name: 'login' })
|
||||
window.location.reload()
|
||||
await nextTick()
|
||||
clearVantOverlays()
|
||||
window.location.assign(router.resolve({ name: 'login' }).href)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
Reference in New Issue
Block a user