diff --git a/components.d.ts b/components.d.ts index ef35dfd..1273ec5 100644 --- a/components.d.ts +++ b/components.d.ts @@ -14,16 +14,13 @@ declare module 'vue' { AInput: typeof import('ant-design-vue/es')['Input'] AInputPassword: typeof import('ant-design-vue/es')['InputPassword'] AModal: typeof import('ant-design-vue/es')['Modal'] - ARadio: typeof import('ant-design-vue/es')['Radio'] ARadioButton: typeof import('ant-design-vue/es')['RadioButton'] ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup'] ASelect: typeof import('ant-design-vue/es')['Select'] ASelectOption: typeof import('ant-design-vue/es')['SelectOption'] ASlider: typeof import('ant-design-vue/es')['Slider'] ASpin: typeof import('ant-design-vue/es')['Spin'] - ASwitch: typeof import('ant-design-vue/es')['Switch'] ATag: typeof import('ant-design-vue/es')['Tag'] - ATextarea: typeof import('ant-design-vue/es')['Textarea'] ATooltip: typeof import('ant-design-vue/es')['Tooltip'] BaseDatePicker: typeof import('./src/components/BasicPacks/BaseDatePicker/src/BaseDatePicker.vue')['default'] Basic: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/basic.vue')['default'] @@ -69,6 +66,5 @@ declare module 'vue' { TransactionSkeleton: typeof import('./src/components/TransactionSkeleton.vue')['default'] VanActionSheet: typeof import('vant/es')['ActionSheet'] VanDialog: typeof import('vant/es')['Dialog'] - VanField: typeof import('vant/es')['Field'] } } diff --git a/src/App.vue b/src/App.vue index ff2a1ad..7798e16 100644 --- a/src/App.vue +++ b/src/App.vue @@ -71,15 +71,23 @@
نسخه جدید منتشر شد!
- -
- پشتیبانی -
- -
+
+
+ + پشتیبانی
-
+
h(LoadingOutlined, { const showSplash = ref(true) const popVisible = ref(false) +const GAROOSI_SUPPORT_URL = 'https://ble.ir/garoosigallery' + +const supportLink = computed(() => { + if (config.config?.name === 'garoosi') { + return GAROOSI_SUPPORT_URL + } + return buildSupportMessengerUrl( + supportMessengerSettings.value.messenger, + supportMessengerSettings.value.messengerId + ) +}) + +const showSupportButton = computed(() => !!supportLink.value) + +const supportIcon = computed(() => { + const messenger = (supportMessengerSettings.value.messenger ?? '').toLowerCase() + switch (messenger) { + case 'whatsapp': + return 'vuesax-linear:whatsapp' + case 'telegram': + return 'vuesax-linear:send-2' + case 'bale': + return 'vuesax-linear:messages-2' + default: + return 'vuesax-linear:messages-2' + } +}) + +const supportAccentColor = computed(() => { + const messenger = (supportMessengerSettings.value.messenger ?? '').toLowerCase() + switch (messenger) { + case 'whatsapp': + return '#22c55e' + case 'telegram': + return '#0ea5e9' + case 'bale': + return '#16a34a' + case 'rubika': + return '#7c3aed' + case 'eitaa': + return '#f59e0b' + case 'soroush': + return '#2563eb' + default: + return config.config?.color || '#334155' + } +}) + +const { + dragging: supportDragging, + fabStyle: supportButtonStyle, + handlePointerDown: onSupportPointerDown, + handlePointerMove: onSupportPointerMove, + handlePointerUp: supportPointerUp, + handlePointerCancel: onSupportPointerCancel +} = useDraggableFloatingButton('khaneghi_support_button_position', { + width: 118, + height: 42, + defaultBottomOffset: 120 +}) + +function openSupportLink() { + if (!supportLink.value) return + window.open(supportLink.value, '_blank', 'noopener,noreferrer') +} + +function handleSupportPointerUp(event: PointerEvent) { + if (supportPointerUp(event)) { + openSupportLink() + } +} + const antTheme = computed(() => { if (!config.config?.color) return null @@ -154,6 +240,7 @@ const antTheme = computed(() => { onMounted(async () => { await config.getConfigData() + await loadSupportMessengerFromSettings() console.log(config?.config?.color) checkVersion() setTimeout(() => { @@ -242,18 +329,49 @@ function update() { #support-button { position: fixed; - bottom: calc(80px + var(--app-credit-footer-offset, 28px) + 12px); - right: 20px; - width: 70px; - height: 70px; - //border-radius: 50%; - display: flex; - justify-content: center; - align-items: center; - //box-shadow: 0 4px 10px rgba(0,0,0,0.3); - cursor: pointer; - transition: transform 0.2s ease, box-shadow 0.2s ease; z-index: 9999; + cursor: grab; + user-select: none; + border-radius: 9999px; + + &.is-dragging { + cursor: grabbing; + } +} + +.support-button-inner { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 9px 14px 9px 12px; + background: #ffffff; + border: 1px solid rgba(15, 23, 42, 0.08); + border-radius: 9999px; + box-shadow: 0 6px 20px rgba(15, 23, 42, 0.14); + color: #1e293b; + font-size: 13px; + font-weight: 600; + line-height: 1; + direction: rtl; + transition: box-shadow 0.2s ease, transform 0.2s ease; + + .support-button-icon { + color: var(--support-accent, #334155); + flex-shrink: 0; + } + + .support-button-label { + white-space: nowrap; + } +} + +#support-button:not(.is-dragging):hover .support-button-inner { + transform: translateY(-1px); + box-shadow: 0 8px 24px rgba(15, 23, 42, 0.18); +} + +#support-button.is-dragging .support-button-inner { + box-shadow: 0 10px 28px rgba(15, 23, 42, 0.22); } @media screen and (min-width: 426px) and (max-width: 767.98px) { diff --git a/src/api/categories.ts b/src/api/categories.ts new file mode 100644 index 0000000..9c1008a --- /dev/null +++ b/src/api/categories.ts @@ -0,0 +1,10 @@ +import useAxios from '@/utils/useAxios' + +export interface StoreCategory { + id: number + name: string + sort_order?: number +} + +export const getStoreCategories = async (): Promise => + await useAxios.get('/customer/categories') diff --git a/src/config-loader.js b/src/config-loader.js index a4078c5..c07d713 100644 --- a/src/config-loader.js +++ b/src/config-loader.js @@ -107,6 +107,22 @@ if (metaTheme) metaTheme.setAttribute('content', splashBg) } + if (config?.user?.name_fa) { + document.title = config.user.name_fa + } + + if (config?.user?.avatar) { + const faviconUrl = config.user.avatar + const existingIcons = document.querySelectorAll('link[rel*="icon"]') + existingIcons.forEach((icon) => icon.remove()) + + const link = document.createElement('link') + link.rel = 'icon' + link.type = 'image/png' + link.href = faviconUrl + '?v=' + Date.now() + document.head.appendChild(link) + } + // Resolve the promise if (configResolve) { configResolve(config) diff --git a/src/models/home.ts b/src/models/home.ts index 8ccf275..82fd025 100644 --- a/src/models/home.ts +++ b/src/models/home.ts @@ -27,6 +27,5 @@ export interface Price { closed?: boolean time?: string main_product?: number - taban_gohar?: number montzar?: number } \ No newline at end of file diff --git a/src/models/products.ts b/src/models/products.ts index 22cc688..c743fe0 100644 --- a/src/models/products.ts +++ b/src/models/products.ts @@ -25,6 +25,7 @@ export interface Product { max_order_cash?: number | null direct_price: number | undefined price_offset_percent?: number | null + use_last_price_if_zero?: boolean reading_method: any created_at: any updated_at: any diff --git a/src/utils/categoryTabs.ts b/src/utils/categoryTabs.ts new file mode 100644 index 0000000..1e6ad1d --- /dev/null +++ b/src/utils/categoryTabs.ts @@ -0,0 +1,7 @@ +import { getStoreCategories } from '@/api/categories' + +/** تب‌های فیلتر محصول — فقط از API دسته‌بندی (هماهنگ با پنل ادمین) */ +export async function loadProductCategoryTabNames(): Promise { + const categories = await getStoreCategories() + return categories.map((item) => item.name) +} diff --git a/src/utils/inputRules.ts b/src/utils/inputRules.ts index a6c9624..01289d7 100644 --- a/src/utils/inputRules.ts +++ b/src/utils/inputRules.ts @@ -32,6 +32,7 @@ import {computed, ref} from 'vue' import type {Rule} from 'ant-design-vue/es/form' import {useWalletStore} from "@/store/walletStore"; import { isCashLikeDealMethod, isMainMetalWalletDealMethod, getInsufficientCashWalletMessage, getInsufficientGoldWalletMessage } from '@/utils/productDealMethod' +import { getCashWalletBalance } from '@/utils/productAssets' export const mobileValidation = (title?: string): Rule => { const label = title ?? 'شماره تلفن' @@ -160,13 +161,14 @@ const checkNumber = async (_rule: Rule, value: number) => { return Promise.reject('استفاده از اعداد اعشاری مجاز نمی باشد') } if (isBuyP){ - if (!walletCash?.walletInfo?.cash && isCashLikeDealMethod(methodP)){ + const availableCash = getCashWalletBalance(walletCash?.walletInfo) + if (!availableCash && isCashLikeDealMethod(methodP)){ return Promise.reject(getInsufficientCashWalletMessage(groupTypeP)) } if (!walletCash?.walletInfo?.gold && isMainMetalWalletDealMethod(methodP)){ return Promise.reject(getInsufficientGoldWalletMessage(groupTypeP, methodP)) } - if (priceP * value > walletCash?.walletInfo?.cash && isCashLikeDealMethod(methodP)) + if (priceP * value > availableCash && isCashLikeDealMethod(methodP)) return Promise.reject(getInsufficientCashWalletMessage(groupTypeP)) if (amountP * value > walletCash?.walletInfo?.gold && isMainMetalWalletDealMethod(methodP)) return Promise.reject(getInsufficientGoldWalletMessage(groupTypeP, methodP)) diff --git a/src/utils/productAssets.ts b/src/utils/productAssets.ts index ca1b347..dbf9a88 100644 --- a/src/utils/productAssets.ts +++ b/src/utils/productAssets.ts @@ -335,17 +335,32 @@ export function getTotalMetalWeight( return Number(walletInfo?.treasure ?? walletInfo?.gold_sum ?? walletInfo?.count_gold ?? 0) } -/** فقط موجودی نقدی کیف اصلی — فیلد gold (مثل 0.3 کیلوگرم) جزو دارایی ریالی نیست */ +/** موجودی ریالی کیف اصلی — با main_wallet_products فیلد cash ممکن است کیف محصول باشد */ export function getRialCashAmount( walletInfo?: Record | null, wallets?: UserWalletLine[] | null ): number { + const aggregated = walletInfo?.count_cash_wallet ?? walletInfo?.count_cash + if (aggregated != null && aggregated !== '') { + const parsed = Number(aggregated) + if (!Number.isNaN(parsed)) { + return parsed + } + } + const mainCash = getMainWalletCash(wallets) if (mainCash > 0) { return mainCash } - return Number(walletInfo?.count_cash ?? walletInfo?.cash ?? 0) + return Number(walletInfo?.cash ?? 0) +} + +export function getCashWalletBalance( + walletInfo?: Record | null, + wallets?: UserWalletLine[] | null +): number { + return getRialCashAmount(walletInfo, wallets) } export function getTotalRialEquivalent(walletInfo?: Record | null): number { diff --git a/src/utils/productCategories.ts b/src/utils/productCategories.ts index 432c972..ee0350f 100644 --- a/src/utils/productCategories.ts +++ b/src/utils/productCategories.ts @@ -7,6 +7,9 @@ export const UNIFIED_PRODUCT_CATEGORIES = [ 'گوی', 'سکه', 'سکه پارسیان', + 'شمش', + 'شمش ونوس', + 'شمش برنا', 'مس', 'نقره', ] as const diff --git a/src/utils/productPrice.ts b/src/utils/productPrice.ts index 6b021e3..75308b6 100644 --- a/src/utils/productPrice.ts +++ b/src/utils/productPrice.ts @@ -2,6 +2,10 @@ import type { Product } from '@/models/products' import { getTradeBuyPrice, getTradeSellPrice, lastPrice } from '@/utils/price' import { isMetalCommodityGroup, isMeltedProductUsingAppPrice, isMeltedTradeGroup } from '@/utils/productCategories' +function productUsesLastPriceIfZero(product: Product): boolean { + return Boolean(product.use_last_price_if_zero) +} + export function resolvePriceOffsetRatio(offsetPercent?: number | null) { const offset = Number(offsetPercent ?? 0) if (!offset) { @@ -35,44 +39,114 @@ export function applyChartPriceOffset(basePrice: number, offsetPercent?: number return Math.round(basePrice + basePrice * ratio) } -/** قیمت خرید نمایشی محصول (همان مقداری که در لیست و اکشن‌شیت نشان داده می‌شود) */ -export function getProductBuyPrice(product: Product): number { +/** قیمت پایه بدون کارمزد — اگر صفر باشد نباید فقط cash_fee به‌عنوان قیمت نمایش داده شود */ +export function getProductBaseBuyPrice(product: Product): number { if (isMeltedProductUsingAppPrice(product)) { - const base = applyPriceOffsetPercent(getTradeBuyPrice(lastPrice.value), product.price_offset_percent) - return base + (product.cash_fee_buy ?? 0) + const apiPrice = Number(product.price ?? 0) + const useLast = productUsesLastPriceIfZero(product) + + if (!useLast) { + const liveBuy = getTradeBuyPrice(lastPrice.value) + if (liveBuy > 0) { + return applyPriceOffsetPercent(liveBuy, product.price_offset_percent) + } + return 0 + } + + if (apiPrice > 0) { + return apiPrice + } + + const liveBuy = getTradeBuyPrice(lastPrice.value) + if (liveBuy > 0) { + return applyPriceOffsetPercent(liveBuy, product.price_offset_percent) + } + + return 0 } if (isMeltedTradeGroup(product.group_type)) { - return (product.price ?? 0) + (product.cash_fee_buy ?? 0) + return Number(product.price ?? 0) } if (isMetalCommodityGroup(product.group_type)) { - const base = (product.price_profit ?? 0) + (product.price ?? 0) - return base + (product.cash_fee_buy ?? 0) + return Number(product.price_profit ?? 0) + Number(product.price ?? 0) } - const basePrice = (product.price ?? 0) + (product.cash_fee_buy ?? 0) - return product.direct_price === 0 - ? basePrice * (product.amount ?? 0) - : basePrice + return Number(product.price ?? 0) } -/** قیمت فروش نمایشی محصول آبشده‌ای */ -export function getProductSellPrice(product: Product): number { +/** قیمت خرید نمایشی محصول (همان مقداری که در لیست و اکشن‌شیت نشان داده می‌شود) */ +export function getProductBuyPrice(product: Product): number { + const base = getProductBaseBuyPrice(product) + if (base <= 0) { + return 0 + } + + const fee = Number(product.cash_fee_buy ?? 0) + + if ( + isMeltedProductUsingAppPrice(product) + || isMeltedTradeGroup(product.group_type) + || isMetalCommodityGroup(product.group_type) + ) { + return base + fee + } + + const withFee = base + fee + return product.direct_price === 0 + ? withFee * Number(product.amount ?? 0) + : withFee +} + +function getProductBaseSellPrice(product: Product): number { if (isMeltedProductUsingAppPrice(product)) { - const base = applyPriceOffsetPercent(getTradeSellPrice(lastPrice.value), product.price_offset_percent) - return base - (product.cash_fee_sell ?? 0) + const apiPrice = Number(product.price ?? 0) + const useLast = productUsesLastPriceIfZero(product) + + if (!useLast) { + const liveSell = getTradeSellPrice(lastPrice.value) + if (liveSell > 0) { + return applyPriceOffsetPercent(liveSell, product.price_offset_percent) + } + return 0 + } + + if (apiPrice > 0) { + return apiPrice + } + + const liveSell = getTradeSellPrice(lastPrice.value) + if (liveSell > 0) { + return applyPriceOffsetPercent(liveSell, product.price_offset_percent) + } + + return 0 } if (isMeltedTradeGroup(product.group_type)) { - return (product.price ?? 0) - (product.cash_fee_sell ?? 0) + return Number(product.price ?? 0) } return getTradeSellPrice(lastPrice.value) } +/** قیمت فروش نمایشی محصول آبشده‌ای */ +export function getProductSellPrice(product: Product): number { + const base = getProductBaseSellPrice(product) + if (base <= 0) { + return 0 + } + + if (isMeltedProductUsingAppPrice(product) || isMeltedTradeGroup(product.group_type)) { + return base - Number(product.cash_fee_sell ?? 0) + } + + return base +} + export function isProductPriceInactive(product: Product): boolean { - return getProductBuyPrice(product) <= 0 + return getProductBaseBuyPrice(product) <= 0 } export { isMeltedTradeGroup, isMeltedProductUsingAppPrice } from '@/utils/productCategories' diff --git a/src/utils/supportMessenger.ts b/src/utils/supportMessenger.ts index 947d74d..b6f648e 100644 --- a/src/utils/supportMessenger.ts +++ b/src/utils/supportMessenger.ts @@ -20,10 +20,16 @@ export const supportMessengerSettings = ref({}) export const DEFAULT_SUPPORT_MESSENGER_TEXT = 'جهت شارژ حساب کاربری خود، کلمه "شماره حساب" را برای پشتیبانی ارسال نمایید.' -function getActiveSettingValue(settings: Array<{ key?: string; is_active?: boolean; value?: unknown }>, key: string) { - const setting = settings.find((item) => item?.key === key && item?.is_active === true) - const value = setting?.value +function getSupportSettingValue( + settings: Array<{ key?: string; is_active?: boolean | number; value?: unknown }>, + key: string +) { + const setting = settings.find((item) => item?.key === key) + if (!setting) return '' + + const value = setting.value if (value === undefined || value === null) return '' + return String(value).trim() } @@ -33,7 +39,7 @@ export function buildSupportMessengerUrl(messenger?: string, messengerId?: strin if (/^https?:\/\//i.test(id)) return id - const type = (messenger ?? '').trim().toLowerCase() + const type = (messenger ?? '').trim().toLowerCase() || 'bale' const username = id.replace(/^@/, '') switch (type) { @@ -53,7 +59,7 @@ export function buildSupportMessengerUrl(messenger?: string, messengerId?: strin case 'soroush': return `https://splus.ir/splus/${username}` default: - return '' + return `https://ble.ir/${username}` } } @@ -63,9 +69,9 @@ export async function loadSupportMessengerFromSettings() { if (!settings || !Array.isArray(settings)) return supportMessengerSettings.value = { - messenger: getActiveSettingValue(settings, 'support_messenger'), - messengerId: getActiveSettingValue(settings, 'support_messenger_id'), - messengerText: getActiveSettingValue(settings, 'support_messenger_text'), + messenger: getSupportSettingValue(settings, 'support_messenger') || 'bale', + messengerId: getSupportSettingValue(settings, 'support_messenger_id'), + messengerText: getSupportSettingValue(settings, 'support_messenger_text'), } } catch (e) { console.log(e) diff --git a/src/utils/useDraggableFloatingButton.ts b/src/utils/useDraggableFloatingButton.ts new file mode 100644 index 0000000..1bb61e8 --- /dev/null +++ b/src/utils/useDraggableFloatingButton.ts @@ -0,0 +1,165 @@ +import { computed, onMounted, onUnmounted, ref } from 'vue' + +const DRAG_THRESHOLD = 6 + +type FabPosition = { x: number; y: number } + +type DraggableFloatingOptions = { + width?: number + height?: number + defaultBottomOffset?: number + defaultRightOffset?: number +} + +export function useDraggableFloatingButton( + storageKey: string, + options: DraggableFloatingOptions = {} +) { + const width = options.width ?? 96 + const height = options.height ?? 44 + const defaultBottomOffset = options.defaultBottomOffset ?? 120 + const defaultRightOffset = options.defaultRightOffset ?? 20 + + const pos = ref({ x: 0, y: 0 }) + const dragging = ref(false) + + let pointerId: number | null = null + let startX = 0 + let startY = 0 + let originX = 0 + let originY = 0 + let hasMoved = false + + const fabStyle = computed(() => ({ + left: `${pos.value.x}px`, + top: `${pos.value.y}px`, + width: `${width}px`, + touchAction: 'none' + })) + + function getViewportWidth() { + return Math.min(window.innerWidth, 426) + } + + function getHorizontalOffset() { + const vw = window.innerWidth + const contentWidth = getViewportWidth() + return Math.max(0, (vw - contentWidth) / 2) + } + + function resetToDefault() { + const vh = window.innerHeight + const offsetX = getHorizontalOffset() + const contentWidth = getViewportWidth() + + pos.value = { + x: offsetX + contentWidth - width - defaultRightOffset, + y: vh - defaultBottomOffset - height + } + } + + function clampToViewport() { + const vw = window.innerWidth + const vh = window.innerHeight + + pos.value = { + x: Math.max(0, Math.min(vw - width, pos.value.x)), + y: Math.max(0, Math.min(vh - height, pos.value.y)) + } + } + + function loadPosition() { + try { + const raw = localStorage.getItem(storageKey) + if (!raw) return false + const parsed = JSON.parse(raw) as FabPosition + if (typeof parsed?.x === 'number' && typeof parsed?.y === 'number') { + pos.value = parsed + clampToViewport() + return true + } + } catch { + // ignore + } + return false + } + + function savePosition() { + localStorage.setItem(storageKey, JSON.stringify(pos.value)) + } + + function handleResize() { + clampToViewport() + savePosition() + } + + function handlePointerDown(event: PointerEvent) { + dragging.value = true + hasMoved = false + pointerId = event.pointerId + startX = event.clientX + startY = event.clientY + originX = pos.value.x + originY = pos.value.y + ;(event.currentTarget as HTMLElement)?.setPointerCapture?.(event.pointerId) + } + + function handlePointerMove(event: PointerEvent) { + if (!dragging.value || pointerId !== event.pointerId) return + + const dx = event.clientX - startX + const dy = event.clientY - startY + + if (Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD) { + hasMoved = true + } + + pos.value = { + x: originX + dx, + y: originY + dy + } + clampToViewport() + } + + function finishPointer(event: PointerEvent) { + if (!dragging.value || pointerId !== event.pointerId) return false + + dragging.value = false + pointerId = null + savePosition() + + const clicked = !hasMoved + hasMoved = false + return clicked + } + + function handlePointerUp(event: PointerEvent) { + return finishPointer(event) + } + + function handlePointerCancel(event: PointerEvent) { + finishPointer(event) + return false + } + + onMounted(() => { + if (!loadPosition()) { + resetToDefault() + } + window.addEventListener('resize', handleResize) + }) + + onUnmounted(() => { + window.removeEventListener('resize', handleResize) + }) + + return { + dragging, + fabStyle, + handlePointerDown, + handlePointerMove, + handlePointerUp, + handlePointerCancel, + resetToDefault + } +} diff --git a/src/views/base/copper&silver/index.vue b/src/views/base/copper&silver/index.vue index a299c29..65369b3 100644 --- a/src/views/base/copper&silver/index.vue +++ b/src/views/base/copper&silver/index.vue @@ -68,14 +68,14 @@
-->
- {{ formatCashWalletBalanceText(usePriceWithoutType(walletStore?.walletInfo?.cash ?? 0), props?.ProductItem?.group_type) }} + {{ formatCashWalletBalanceText(usePriceWithoutType(cashWalletBalance), props?.ProductItem?.group_type) }}
+ getCashWalletBalance(walletStore.walletInfo, userStore.userInfo?.wallets) +) const fundStore = useFundUserStore() const confirmAction = ref(false) const noCompleteVisible = ref(false) @@ -490,7 +494,7 @@ const unitGoldCalculator = computed(() => { }) const unitCashCalculator = computed(() => { - return (walletStore?.walletInfo?.cash ?? 0) / 100 + return cashWalletBalance.value / 100 }) const limitedPrice = computed(() => { diff --git a/src/views/base/gold/index.vue b/src/views/base/gold/index.vue index 3487c94..3c2d54b 100644 --- a/src/views/base/gold/index.vue +++ b/src/views/base/gold/index.vue @@ -81,14 +81,14 @@ -->
- {{ formatCashWalletBalanceText(usePriceWithoutType(walletStore?.walletInfo?.cash ?? 0), props?.ProductItem?.group_type) }} + {{ formatCashWalletBalanceText(usePriceWithoutType(cashWalletBalance), props?.ProductItem?.group_type) }}
-
موجودی کیف پول ناکافی می باشد.
@@ -297,7 +297,7 @@ import { useFundUserStore } from '@/store/fundStore' import {useConfigStore} from "@/store/configStore"; import { calcFeeInGrams, loadServiceFeeFromSettings } from '@/utils/serviceFee' import { formatCashWalletBalanceText, getWeightUnit } from '@/utils/productDealMethod' -import { formatProductWalletBalanceText, getProductInventoryQuantity } from '@/utils/productAssets' +import { formatProductWalletBalanceText, getCashWalletBalance, getProductInventoryQuantity } from '@/utils/productAssets' import { useWalletAssets } from '@/composables/useWalletAssets' import { isMeltedTradeGroup } from '@/utils/productCategories' import { useSettingsStore } from '@/store/settingsStore' @@ -316,6 +316,9 @@ const { meltedGoldLabel } = storeToRefs(settingsStore) const weightUnit = computed(() => getWeightUnit()) const fundStore = useFundUserStore() const { assetsSummary, loadWalletAssets } = useWalletAssets() +const cashWalletBalance = computed(() => + getCashWalletBalance(walletStore.walletInfo, userStore.userInfo?.wallets) +) const confirmAction = ref(false) const noCompleteVisible = ref(false) const buyItems = ref([]) @@ -576,7 +579,7 @@ function percentChange(val: number) { const unitGoldCalculator = computed(() => productWalletBalance.value / 100) const unitCashCalculator = computed(() => { - return (walletStore?.walletInfo?.cash ?? 0) / 100 + return cashWalletBalance.value / 100 }) const limitedPrice = computed(() => { diff --git a/src/views/base/home/components/PriceStats.vue b/src/views/base/home/components/PriceStats.vue index 6bff178..5fedcd8 100644 --- a/src/views/base/home/components/PriceStats.vue +++ b/src/views/base/home/components/PriceStats.vue @@ -49,4 +49,4 @@ const props = withDefaults( const highPrice = computed(() => props.price?.up?.buy ?? getTradeBuyPrice(props.price)) const lowPrice = computed(() => props.price?.up?.sell ?? getTradeSellPrice(props.price)) - \ No newline at end of file + diff --git a/src/views/base/products/components/DealProduct.vue b/src/views/base/products/components/DealProduct.vue index 4a1b940..d41776c 100644 --- a/src/views/base/products/components/DealProduct.vue +++ b/src/views/base/products/components/DealProduct.vue @@ -296,7 +296,7 @@ import { productMinOrderCash } from '@/utils/productOrderLimit' import { useWalletAssets } from '@/composables/useWalletAssets' -import { formatProductWalletBalanceText, getProductInventoryQuantity } from '@/utils/productAssets' +import { formatProductWalletBalanceText, getCashWalletBalance, getProductInventoryQuantity } from '@/utils/productAssets' const typeDeal = computed(() => getProductDealMethodOptions(props?.ProductItem?.group_type, isBuy.value)) @@ -352,16 +352,20 @@ const buyMetalWalletBalanceText = computed(() => : goldWalletBalanceText.value ) -const cashWalletBalanceText = computed(() => - formatCashWalletBalanceText( - usePriceWithoutType(walletStore?.walletInfo?.cash ?? 0), - productGroupType.value - ) -) const infoVisible = ref(false) const walletStore = useWalletStore() const { assetsSummary, loadWalletAssets } = useWalletAssets() const userStore = useUserStore() +const cashWalletBalance = computed(() => + getCashWalletBalance(walletStore.walletInfo, userStore.userInfo?.wallets) +) + +const cashWalletBalanceText = computed(() => + formatCashWalletBalanceText( + usePriceWithoutType(cashWalletBalance.value), + productGroupType.value + ) +) const confirmAction = ref(false) const noCompleteVisible = ref(false) const buyItems = ref([]) @@ -622,7 +626,7 @@ const unitGoldCalculator = computed(() => { }) const unitCashCalculator = computed(() => { - return (walletStore?.walletInfo?.cash ?? 0) / 100 + return cashWalletBalance.value / 100 }) const limitedPrice = computed(() => { diff --git a/src/views/base/products/components/ProductItem.vue b/src/views/base/products/components/ProductItem.vue index db94b2f..8a36820 100644 --- a/src/views/base/products/components/ProductItem.vue +++ b/src/views/base/products/components/ProductItem.vue @@ -11,8 +11,7 @@ -
{{usePrice(goldPriceValue)}}
-
{{usePrice(price)}}
+
{{ usePrice(getProductBuyPrice(props.ProductItem)) }}
@@ -33,7 +32,6 @@ import {usePrice} from '@/utils/usePrice'; import {getFileUrl} from '@/utils/file'; import {computed, ref} from "vue"; import { getProductBuyPrice } from '@/utils/productPrice' -import { isMeltedTradeGroup } from '@/utils/productCategories' interface Prop { ProductItem: Product @@ -68,29 +66,6 @@ const displayImage = computed(() => { if (fileUrl) return fileUrl return defaultImages[props.ProductItem.group_type] ?? defaultImg }) -// const typeOfProduct=(val:string)=>{ -// if (val.includes('گوی')) -// return 'گوی' -// if (val.includes('شمش')) -// return 'شمش' -// if (val.includes('مکعب')) -// return 'مکعب' -// if (val.includes('24 عیار')) -// return '24 عیار' -// -// return 'آب شده' -// } - -const price = computed(()=>{ - if(props.ProductItem.direct_price === 0){ - return ((props.ProductItem.price ?? 0) + (props.ProductItem.cash_fee_buy ?? 0)) * props.ProductItem.amount - } - else { - return ((props.ProductItem.price ?? 0) + (props.ProductItem.cash_fee_buy ?? 0)) - } -}) - -const goldPriceValue = computed(() => getProductBuyPrice(props.ProductItem)) diff --git a/src/views/base/products/composables/useProductsPageData.ts b/src/views/base/products/composables/useProductsPageData.ts index aaaaf36..9ebbe86 100644 --- a/src/views/base/products/composables/useProductsPageData.ts +++ b/src/views/base/products/composables/useProductsPageData.ts @@ -3,11 +3,14 @@ import { getProductsCatalog } from '@/composables/useProductsCatalog' import { loading } from '@/utils' import type { Product } from '@/models/products' import { loadServiceFeeFromSettings } from '@/utils/serviceFee' +import { loadProductCategoryTabNames } from '@/utils/categoryTabs' +import { getLastPrices, startLastPricePolling, stopLastPricePolling } from '@/utils/price' /** Shared products page datasets — mobile/desktop views read this module state only. */ let initPromise: Promise | null = null let productListInterval: ReturnType | undefined let providerMountCount = 0 +let pricePollingMountCount = 0 const initialized = ref(false) @@ -40,6 +43,17 @@ function syncTypeItems(products: Product[]) { }) } +async function loadCategoryTabs() { + try { + const tabs = await loadProductCategoryTabNames() + if (tabs.length) { + productsTypeItems.value = tabs + } + } catch (error) { + console.error('Product category tabs error:', error) + } +} + function applyProductsList(products: Product[]) { if (products.length) { productsCloseTime.value = products[0].auto_close_at @@ -100,10 +114,19 @@ function stopProductListPolling() { export function onProductsPageProviderMounted() { providerMountCount += 1 + pricePollingMountCount += 1 + if (pricePollingMountCount === 1) { + void getLastPrices() + startLastPricePolling() + } } export function onProductsPageProviderUnmounted() { providerMountCount = Math.max(0, providerMountCount - 1) + pricePollingMountCount = Math.max(0, pricePollingMountCount - 1) + if (pricePollingMountCount === 0) { + stopLastPricePolling() + } if (providerMountCount === 0) { stopProductListPolling() } @@ -124,7 +147,10 @@ export async function initProductsPageData(force = false) { loading.value = true const products = await getProductsCatalog(force) applyProductsList(products ?? []) - await loadServiceFeeFromSettings() + await Promise.all([ + loadServiceFeeFromSettings(), + loadCategoryTabs(), + ]) initialized.value = true startProductListPolling() } catch (error) { @@ -142,6 +168,8 @@ export function resetProductsPageData() { initialized.value = false initPromise = null providerMountCount = 0 + pricePollingMountCount = 0 + stopLastPricePolling() stopProductListPolling() productsActiveKey.value = 'همه' productsTypeItems.value = []