diff --git a/src/App.vue b/src/App.vue index 92704c8..5e92074 100644 --- a/src/App.vue +++ b/src/App.vue @@ -208,10 +208,11 @@ const { handlePointerMove: onSupportPointerMove, handlePointerUp: supportPointerUp, handlePointerCancel: onSupportPointerCancel -} = useDraggableFloatingButton('khaneghi_support_button_position', { +} = useDraggableFloatingButton('khaneghi_support_button_position_v2', { width: 118, height: 42, - defaultBottomOffset: 120 + defaultBottomGap: 12, + defaultRightOffset: 16 }) function openSupportLink() { @@ -322,7 +323,7 @@ function update() { #support-button { position: fixed; - z-index: 9999; + z-index: 5100; cursor: grab; user-select: none; border-radius: 9999px; diff --git a/src/utils/categoryTabs.ts b/src/utils/categoryTabs.ts index 1e6ad1d..6fe6b4d 100644 --- a/src/utils/categoryTabs.ts +++ b/src/utils/categoryTabs.ts @@ -1,6 +1,6 @@ import { getStoreCategories } from '@/api/categories' -/** تب‌های فیلتر محصول — فقط از API دسته‌بندی (هماهنگ با پنل ادمین) */ +/** تب‌های فیلتر محصول — دسته‌های فعال که محصول دارند (خودکار از API) */ export async function loadProductCategoryTabNames(): Promise { const categories = await getStoreCategories() return categories.map((item) => item.name) diff --git a/src/utils/supportMessenger.ts b/src/utils/supportMessenger.ts index b6f648e..9516c3f 100644 --- a/src/utils/supportMessenger.ts +++ b/src/utils/supportMessenger.ts @@ -17,9 +17,6 @@ export interface SupportMessengerSettings { export const supportMessengerSettings = ref({}) -export const DEFAULT_SUPPORT_MESSENGER_TEXT = - 'جهت شارژ حساب کاربری خود، کلمه "شماره حساب" را برای پشتیبانی ارسال نمایید.' - function getSupportSettingValue( settings: Array<{ key?: string; is_active?: boolean | number; value?: unknown }>, key: string @@ -39,9 +36,11 @@ export function buildSupportMessengerUrl(messenger?: string, messengerId?: strin if (/^https?:\/\//i.test(id)) return id - const type = (messenger ?? '').trim().toLowerCase() || 'bale' + const type = (messenger ?? '').trim().toLowerCase() const username = id.replace(/^@/, '') + if (!type) return '' + switch (type) { case 'bale': if (id.includes('ble.ir')) { @@ -59,7 +58,7 @@ export function buildSupportMessengerUrl(messenger?: string, messengerId?: strin case 'soroush': return `https://splus.ir/splus/${username}` default: - return `https://ble.ir/${username}` + return '' } } @@ -69,7 +68,7 @@ export async function loadSupportMessengerFromSettings() { if (!settings || !Array.isArray(settings)) return supportMessengerSettings.value = { - messenger: getSupportSettingValue(settings, 'support_messenger') || 'bale', + messenger: getSupportSettingValue(settings, 'support_messenger'), messengerId: getSupportSettingValue(settings, 'support_messenger_id'), messengerText: getSupportSettingValue(settings, 'support_messenger_text'), } diff --git a/src/utils/useDraggableFloatingButton.ts b/src/utils/useDraggableFloatingButton.ts index 1bb61e8..075ceda 100644 --- a/src/utils/useDraggableFloatingButton.ts +++ b/src/utils/useDraggableFloatingButton.ts @@ -1,24 +1,54 @@ import { computed, onMounted, onUnmounted, ref } from 'vue' const DRAG_THRESHOLD = 6 +const POSITION_VERSION = 1 type FabPosition = { x: number; y: number } +type SavedFabOffset = { + version: number + dx: number + dy: number +} + type DraggableFloatingOptions = { width?: number height?: number - defaultBottomOffset?: number + /** فاصله از پایینِ نوار منو + montzar (px) — مثل تصویر */ + defaultBottomGap?: number defaultRightOffset?: number } +function readBottomChromeHeight(): number { + const styles = getComputedStyle(document.documentElement) + const direct = parseFloat(styles.getPropertyValue('--app-bottom-chrome-height')) + if (!Number.isNaN(direct) && direct > 0) { + return direct + } + + const nav = parseFloat(styles.getPropertyValue('--app-nav-height')) || 80 + const creditOffset = parseFloat(styles.getPropertyValue('--app-credit-footer-offset')) + if (!Number.isNaN(creditOffset) && creditOffset > 0) { + return nav + creditOffset + } + + const probe = document.createElement('div') + probe.style.cssText = 'position:fixed;visibility:hidden;pointer-events:none;height:var(--app-bottom-chrome-height)' + document.body.appendChild(probe) + const measured = probe.getBoundingClientRect().height + document.body.removeChild(probe) + + return measured > 0 ? measured : nav + 28 +} + 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 defaultBottomGap = options.defaultBottomGap ?? 12 + const defaultRightOffset = options.defaultRightOffset ?? 16 const pos = ref({ x: 0, y: 0 }) const dragging = ref(false) @@ -44,26 +74,40 @@ export function useDraggableFloatingButton( function getHorizontalOffset() { const vw = window.innerWidth const contentWidth = getViewportWidth() + return Math.max(0, (vw - contentWidth) / 2) } - function resetToDefault() { + function getDefaultPosition(): FabPosition { const vh = window.innerHeight const offsetX = getHorizontalOffset() const contentWidth = getViewportWidth() + const bottomChrome = readBottomChromeHeight() - pos.value = { + return { x: offsetX + contentWidth - width - defaultRightOffset, - y: vh - defaultBottomOffset - height + y: vh - bottomChrome - defaultBottomGap - height } } + function applySavedOffset(dx: number, dy: number) { + const base = getDefaultPosition() + pos.value = { + x: base.x + dx, + y: base.y + dy + } + clampToViewport() + } + function clampToViewport() { - const vw = window.innerWidth const vh = window.innerHeight + const offsetX = getHorizontalOffset() + const contentWidth = getViewportWidth() + const minX = offsetX + const maxX = offsetX + contentWidth - width pos.value = { - x: Math.max(0, Math.min(vw - width, pos.value.x)), + x: Math.max(minX, Math.min(maxX, pos.value.x)), y: Math.max(0, Math.min(vh - height, pos.value.y)) } } @@ -72,25 +116,56 @@ export function useDraggableFloatingButton( 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() + + const parsed = JSON.parse(raw) as SavedFabOffset + if ( + parsed?.version === POSITION_VERSION + && typeof parsed.dx === 'number' + && typeof parsed.dy === 'number' + ) { + applySavedOffset(parsed.dx, parsed.dy) return true } } catch { // ignore } + return false } function savePosition() { - localStorage.setItem(storageKey, JSON.stringify(pos.value)) + const base = getDefaultPosition() + const payload: SavedFabOffset = { + version: POSITION_VERSION, + dx: pos.value.x - base.x, + dy: pos.value.y - base.y + } + localStorage.setItem(storageKey, JSON.stringify(payload)) + } + + function resetToDefault() { + pos.value = getDefaultPosition() } function handleResize() { - clampToViewport() - savePosition() + try { + const raw = localStorage.getItem(storageKey) + if (raw) { + const parsed = JSON.parse(raw) as SavedFabOffset + if ( + parsed?.version === POSITION_VERSION + && typeof parsed.dx === 'number' + && typeof parsed.dy === 'number' + ) { + applySavedOffset(parsed.dx, parsed.dy) + return + } + } + } catch { + // ignore + } + + resetToDefault() } function handlePointerDown(event: PointerEvent) { diff --git a/src/utils/usePrice.ts b/src/utils/usePrice.ts index f69748d..0bf0c34 100644 --- a/src/utils/usePrice.ts +++ b/src/utils/usePrice.ts @@ -24,3 +24,58 @@ export function useTowDecimal(num: string | number): string | number { export function useFormat(num: string | number, unit = '') { return usePrice(Math.abs(+useTowDecimal(num)), true, unit) } + +type TransactionPriceSource = { + cash?: number | null + gold?: number | null + order?: { + price?: number | null + total_price?: number | null + amount?: number | null + } | null +} + +export function getTransactionTradePrice(item: TransactionPriceSource): number { + const orderPrice = Number(item.order?.price) + if (Number.isFinite(orderPrice) && orderPrice > 0) { + return Math.round(orderPrice) + } + + const cashAmount = Math.abs(Number(item.cash) || 0) + const goldAmount = + Math.abs(Number(item.gold) || 0) || Math.abs(Number(item.order?.amount) || 0) + + if (goldAmount > 0 && cashAmount > 0) { + return Math.round(cashAmount / goldAmount) + } + + const totalPrice = Number(item.order?.total_price) + if (Number.isFinite(totalPrice) && totalPrice > 0) { + return Math.round(totalPrice) + } + + return 0 +} + +export function getTransactionGoldAmount(item: TransactionPriceSource): number { + const gold = Number(item.gold) || 0 + if (Math.abs(gold) > 0) { + return gold + } + + const orderAmount = Number(item.order?.amount) || 0 + if (orderAmount > 0) { + const cash = Number(item.cash) || 0 + if (cash < 0) return orderAmount + if (cash > 0) return -orderAmount + return orderAmount + } + + return gold +} + +export function formatGoldAmount(amount?: number | null): string { + const num = Number(amount) + if (!Number.isFinite(num)) return '0' + return String(Number(num.toFixed(3))) +} diff --git a/src/views/base/home/composables/usePriceChart.ts b/src/views/base/home/composables/usePriceChart.ts index c24be2f..6701fcf 100644 --- a/src/views/base/home/composables/usePriceChart.ts +++ b/src/views/base/home/composables/usePriceChart.ts @@ -1,6 +1,13 @@ import { ref } from 'vue' import { getAllPrices } from '@/api/home' +const MIN_SCALED_CHART_VALUE = 30_000 +const MIN_FULL_CHART_VALUE = 30_000_000 + +function isValidChartValue(value: number): boolean { + return value > MIN_SCALED_CHART_VALUE || value > MIN_FULL_CHART_VALUE +} + export function usePriceChart() { const priceChartSeries = ref([{ name: 'قیمت', data: [] }]) const selection = ref('DAILY') @@ -16,7 +23,18 @@ export function usePriceChart() { const response = await getAllPrices() const data = response.data?.prices const selectedData = data[selection.value] || [] - priceChartSeries.value[0].data = selectedData.map((item: any) => [item.timestamp, item.value - 1]) + let lastValid = 0 + priceChartSeries.value[0].data = selectedData + .map((item: { timestamp: number; value: number }) => { + let value = Number(item.value) + if (!isValidChartValue(value)) { + value = lastValid + } else { + lastValid = value + } + return [item.timestamp, value] as [number, number] + }) + .filter((point: [number, number]) => point[1] > 0) } catch (error) { console.error('Error fetching prices:', error) } finally { diff --git a/src/views/base/transactions/components/Item.vue b/src/views/base/transactions/components/Item.vue index ba318bb..f97f508 100644 --- a/src/views/base/transactions/components/Item.vue +++ b/src/views/base/transactions/components/Item.vue @@ -12,8 +12,8 @@
{{ toJalaliOutOfUTC(item.expire_at) }}
-
{{ item.gold }} +
{{ formatGoldAmount(getTransactionGoldAmount(item)) }}
{{item.unit}}
@@ -29,7 +29,7 @@
قیمت معامله:
-
{{ usePriceWithoutType(parseInt((Math.abs(item.cash) / Math.abs(item.gold)).toFixed(0))) }}
+
{{ usePriceWithoutType(getTransactionTradePrice(item)) }}
ریال
@@ -62,16 +62,16 @@
مقدار {{ item.wallet?.product ? item.wallet.product.title : getMeltedGoldTradeLabel() }} مبادله شده:
-
+
-
{{ item.gold }}
+
{{ formatGoldAmount(getTransactionGoldAmount(item)) }}
{{ item.unit }}
- {{ getProductTotalWeightLabel(Math.abs(item.gold ?? 0), item.wallet.product) }} + {{ getProductTotalWeightLabel(Math.abs(getTransactionGoldAmount(item)), item.wallet.product) }}
@@ -146,7 +146,7 @@