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 = []