diff --git a/src/App.vue b/src/App.vue
index 2073315..d99d54f 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -48,8 +48,9 @@
+
+
@@ -77,10 +85,13 @@ import img_pattern from '@/views/login/assets/img/pattern.svg'
import { LoadingOutlined } from '@ant-design/icons-vue'
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import {
- buildSupportMessengerUrl,
loadSupportMessengerFromSettings,
+ openSupportMessengerEntry,
+ resolveSupportMessengerEntries,
+ supportMessengerPickerOpen,
supportMessengerSettings,
} from '@/utils/supportMessenger'
+import SupportMessengerPicker from '@/components/SupportMessengerPicker.vue'
import { useDraggableFloatingButton } from '@/utils/useDraggableFloatingButton'
import { useApplyDesktopLayoutClass } from '@/layouts/composables/useAppLayout'
@@ -102,48 +113,41 @@ const showSplash = ref(true)
useApplyDesktopLayoutClass()
-const supportLink = computed(() =>
- buildSupportMessengerUrl(
- supportMessengerSettings.value.messenger,
- supportMessengerSettings.value.messengerId
- )
+const supportButtonEl = ref(null)
+const supportPickerOpen = supportMessengerPickerOpen
+
+const supportMessengerEntries = computed(() =>
+ resolveSupportMessengerEntries(supportMessengerSettings.value),
)
-const showSupportButton = computed(() => !!supportLink.value)
+const showSupportButton = computed(() => supportMessengerEntries.value.length > 0)
-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 primaryEntry = computed(() => supportMessengerEntries.value[0] ?? null)
-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 colors.primary || '#334155'
+const supportIcon = computed(() => primaryEntry.value?.icon ?? 'vuesax-linear:messages-2')
+
+const supportAccentColor = computed(() =>
+ (primaryEntry.value?.accent ?? colors.primary) || '#334155',
+)
+
+let supportPickerOpenedAt = 0
+
+function openSupportLink() {
+ const entries = supportMessengerEntries.value
+ if (entries.length === 0) return
+ if (entries.length === 1) {
+ void openSupportMessengerEntry(entries[0])
+ return
}
-})
+ const now = Date.now()
+ if (supportPickerOpen.value) {
+ if (now - supportPickerOpenedAt < 450) return
+ supportPickerOpen.value = false
+ return
+ }
+ supportPickerOpen.value = true
+ supportPickerOpenedAt = now
+}
const {
dragging: supportDragging,
@@ -159,15 +163,15 @@ const {
defaultRightOffset: 16,
})
-function openSupportLink() {
- if (!supportLink.value) return
- window.open(supportLink.value, '_blank', 'noopener,noreferrer')
+function openSupportLinkHandler() {
+ openSupportLink()
}
function handleSupportPointerUp(event: PointerEvent) {
- if (supportPointerUp(event)) {
- openSupportLink()
- }
+ const clicked = supportPointerUp(event)
+ if (!clicked) return
+ event.stopPropagation()
+ openSupportLinkHandler()
}
onMounted(async () => {
@@ -232,4 +236,9 @@ onMounted(async () => {
#support-button.is-dragging .support-button-inner {
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.22);
}
+
+#support-button.is-picker-open .support-button-inner {
+ transform: scale(0.94);
+ box-shadow: 0 4px 14px rgba(15, 23, 42, 0.12);
+}
diff --git a/src/assets/messengers/bale.png b/src/assets/messengers/bale.png
new file mode 100644
index 0000000..7b41254
Binary files /dev/null and b/src/assets/messengers/bale.png differ
diff --git a/src/assets/messengers/eitaa.png b/src/assets/messengers/eitaa.png
new file mode 100644
index 0000000..87d0cef
Binary files /dev/null and b/src/assets/messengers/eitaa.png differ
diff --git a/src/assets/messengers/rubika.png b/src/assets/messengers/rubika.png
new file mode 100644
index 0000000..d764418
Binary files /dev/null and b/src/assets/messengers/rubika.png differ
diff --git a/src/assets/messengers/soroush.svg b/src/assets/messengers/soroush.svg
new file mode 100644
index 0000000..f3dee3e
--- /dev/null
+++ b/src/assets/messengers/soroush.svg
@@ -0,0 +1,7 @@
+
diff --git a/src/assets/messengers/telegram.svg b/src/assets/messengers/telegram.svg
new file mode 100644
index 0000000..53458c5
--- /dev/null
+++ b/src/assets/messengers/telegram.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/messengers/whatsapp.svg b/src/assets/messengers/whatsapp.svg
new file mode 100644
index 0000000..b2444e2
--- /dev/null
+++ b/src/assets/messengers/whatsapp.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/SupportMessengerBrandIcon.vue b/src/components/SupportMessengerBrandIcon.vue
new file mode 100644
index 0000000..5878d33
--- /dev/null
+++ b/src/components/SupportMessengerBrandIcon.vue
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/SupportMessengerPicker.vue b/src/components/SupportMessengerPicker.vue
new file mode 100644
index 0000000..0208efa
--- /dev/null
+++ b/src/components/SupportMessengerPicker.vue
@@ -0,0 +1,293 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/config/app-config.ts b/src/config/app-config.ts
index c8f1785..aace9cf 100644
--- a/src/config/app-config.ts
+++ b/src/config/app-config.ts
@@ -4,10 +4,56 @@ type RawAppConfig = AppConfig & {
user?: Record
}
+const EMPTY_STRING_CONFIG_KEYS = new Set([
+ 'color',
+ 'secondary_color',
+ 'background_color',
+ 'theme_config',
+])
+
+function emptyStringToNull(value: unknown) {
+ if (typeof value === 'string' && value.trim() === '') {
+ return null
+ }
+
+ return value
+}
+
+function normalizeUserConfig(user?: Record) {
+ if (!user || typeof user !== 'object') {
+ return user
+ }
+
+ const normalized: Record = { ...user }
+
+ for (const key of EMPTY_STRING_CONFIG_KEYS) {
+ if (key in normalized) {
+ normalized[key] = emptyStringToNull(normalized[key])
+ }
+ }
+
+ return normalized
+}
+
+export function normalizeAppConfig(config: RawAppConfig): RawAppConfig {
+ if (!config || typeof config !== 'object') {
+ return {}
+ }
+
+ if (!config.user || typeof config.user !== 'object') {
+ return config
+ }
+
+ return {
+ ...config,
+ user: normalizeUserConfig(config.user),
+ }
+}
+
let appConfig: RawAppConfig | null = null
export function setAppConfig(config: RawAppConfig) {
- appConfig = config || {}
+ appConfig = normalizeAppConfig(config || {})
window.__APP_CONFIG__ = appConfig
}
@@ -31,7 +77,7 @@ export function getTenantConfig() {
export function getConfig(key: string, fallback?: T): T {
const config = getTenantConfig()
- const value = config?.[key]
+ const value = emptyStringToNull(config?.[key])
return (value ?? fallback ?? null) as T
}
diff --git a/src/config/load-config.ts b/src/config/load-config.ts
index b73be78..0b9e82b 100644
--- a/src/config/load-config.ts
+++ b/src/config/load-config.ts
@@ -1,4 +1,5 @@
import type { AppConfig } from '@/config/types'
+import { normalizeAppConfig } from '@/config/app-config'
import { readCachedAppConfig, writeCachedAppConfig } from '@/config/config-cache'
const CONFIG_SUCCESS_URL_KEY = 'CONFIG_Success_URL'
@@ -60,7 +61,7 @@ async function fetchConfig(url: string, timeout = 3000): Promise {
throw new Error('Config response is not JSON')
}
- return await response.json()
+ return normalizeAppConfig(await response.json())
} finally {
clearTimeout(timeoutId)
}
@@ -95,7 +96,7 @@ export async function loadAppConfig(): Promise {
const cachedConfig = readCachedAppConfig()
if (cachedConfig) {
- return cachedConfig
+ return normalizeAppConfig(cachedConfig)
}
return await fetchFreshAppConfig()
diff --git a/src/utils/price.ts b/src/utils/price.ts
index 3863515..75fc0ce 100644
--- a/src/utils/price.ts
+++ b/src/utils/price.ts
@@ -15,7 +15,7 @@ let lastSubscribers = 0
export const price = ref()
export const lastPrice = ref()
-export const olderPrice = ref(0)
+export const olderPrice = ref(null)
let lastPriceSourceAt = 0
let consecutivePriceFetchFailures = 0
@@ -162,13 +162,17 @@ export async function getLastPrices(): Promise {
export function applyLastPricePayload(dataLast: Price) {
const normalized = normalizePriceResponse(dataLast)
- if (lastPrice.value?.price_last != normalized.price_last) {
- olderPrice.value = lastPrice.value?.price_last
+ const nextBuy = getTradeBuyPrice(normalized)
+ const prevBuy = getTradeBuyPrice(lastPrice.value)
+
+ if (prevBuy > 0 && nextBuy > 0 && prevBuy !== nextBuy) {
+ olderPrice.value = prevBuy
}
+
lastPrice.value = normalized
price.value = {
...normalized,
- buy: getTradeBuyPrice(normalized),
+ buy: nextBuy,
sell: getTradeSellPrice(normalized),
}
@@ -177,9 +181,13 @@ export function applyLastPricePayload(dataLast: Price) {
}
}
+/** تغییر درصدی نسبت به قیمت خرید قبلی (آخرین بهروزرسانی) */
export function percentPriceChanged() {
- if (!olderPrice.value) {
+ const currentBuy = getTradeBuyPrice(lastPrice.value)
+ const prevBuy = olderPrice.value
+ if (!currentBuy || prevBuy == null || prevBuy <= 0 || prevBuy === currentBuy) {
return 0
}
- return ((lastPrice.value?.price_last! - olderPrice.value) * 100) / olderPrice.value
+
+ return ((currentBuy - prevBuy) * 100) / prevBuy
}
diff --git a/src/utils/supportMessenger.ts b/src/utils/supportMessenger.ts
index 9516c3f..91a357e 100644
--- a/src/utils/supportMessenger.ts
+++ b/src/utils/supportMessenger.ts
@@ -9,28 +9,111 @@ export type SupportMessengerType =
| 'eitaa'
| 'soroush'
+export interface SupportMessengerEntry {
+ messenger: string
+ id: string
+}
+
export interface SupportMessengerSettings {
+ messengers?: SupportMessengerEntry[]
messenger?: SupportMessengerType | string
messengerId?: string
messengerText?: string
}
+export interface SupportMessengerMeta {
+ label: string
+ icon: string
+ accent: string
+}
+
+export interface ResolvedSupportMessengerEntry extends SupportMessengerEntry {
+ label: string
+ icon: string
+ accent: string
+ url: string
+}
+
export const supportMessengerSettings = ref({})
+export const supportMessengerPickerOpen = ref(false)
+
+const VALID_SUPPORT_MESSENGERS = new Set([
+ 'bale',
+ 'telegram',
+ 'whatsapp',
+ 'rubika',
+ 'eitaa',
+ 'soroush',
+])
+
+export const SUPPORT_MESSENGER_META: Record = {
+ bale: { label: 'بله', icon: 'vuesax-linear:messages-2', accent: '#16a34a' },
+ telegram: { label: 'تلگرام', icon: 'vuesax-linear:send-2', accent: '#0ea5e9' },
+ whatsapp: { label: 'واتساپ', icon: 'vuesax-linear:whatsapp', accent: '#22c55e' },
+ rubika: { label: 'روبیکا', icon: 'vuesax-linear:messages-2', accent: '#7c3aed' },
+ eitaa: { label: 'ایتا', icon: 'vuesax-linear:messages-2', accent: '#f59e0b' },
+ soroush: { label: 'سروش', icon: 'vuesax-linear:messages-2', accent: '#2563eb' },
+}
function getSupportSettingValue(
- settings: Array<{ key?: string; is_active?: boolean | number; value?: unknown }>,
- key: string
+ settings: Array<{ key?: string; 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()
}
-export function buildSupportMessengerUrl(messenger?: string, messengerId?: string) {
+export function parseSupportMessengersJson(raw?: string | null): SupportMessengerEntry[] {
+ const value = (raw ?? '').trim()
+ if (!value) return []
+
+ try {
+ const decoded = JSON.parse(value)
+ if (!Array.isArray(decoded)) return []
+
+ return decoded
+ .map((row) => ({
+ messenger: String(row?.messenger ?? '').trim().toLowerCase(),
+ id: String(row?.id ?? '').trim(),
+ }))
+ .filter((row) => VALID_SUPPORT_MESSENGERS.has(row.messenger) && row.id !== '')
+ } catch {
+ return []
+ }
+}
+
+export function resolveSupportMessengersFromSettings(
+ settings: Array<{ key?: string; value?: unknown }>,
+): SupportMessengerEntry[] {
+ const fromJson = parseSupportMessengersJson(getSupportSettingValue(settings, 'support_messengers'))
+ if (fromJson.length > 0) return fromJson
+
+ const legacyMessenger = getSupportSettingValue(settings, 'support_messenger').toLowerCase()
+ const legacyId = getSupportSettingValue(settings, 'support_messenger_id')
+ if (!legacyId) return []
+
+ const messenger = legacyMessenger || 'bale'
+ if (!VALID_SUPPORT_MESSENGERS.has(messenger)) return []
+
+ return [{ messenger, id: legacyId }]
+}
+
+export function resolveSupportMessengerMeta(messenger?: string): SupportMessengerMeta {
+ const type = (messenger ?? '').trim().toLowerCase()
+ return SUPPORT_MESSENGER_META[type] ?? {
+ label: 'پشتیبانی',
+ icon: 'vuesax-linear:headphone',
+ accent: '#334155',
+ }
+}
+
+export function buildSupportMessengerUrl(
+ messenger?: string,
+ messengerId?: string,
+) {
const id = (messengerId ?? '').trim()
if (!id) return ''
@@ -62,12 +145,48 @@ export function buildSupportMessengerUrl(messenger?: string, messengerId?: strin
}
}
+export function resolveSupportMessengerEntries(
+ settings?: SupportMessengerSettings,
+): ResolvedSupportMessengerEntry[] {
+ const resolved = settings ?? supportMessengerSettings.value
+ const source = resolved.messengers?.length
+ ? resolved.messengers
+ : resolveLegacySingleMessenger(resolved)
+
+ return source
+ .map((entry) => {
+ const meta = resolveSupportMessengerMeta(entry.messenger)
+ const url = buildSupportMessengerUrl(entry.messenger, entry.id)
+ if (!url) return null
+ return { ...entry, ...meta, url }
+ })
+ .filter((entry): entry is ResolvedSupportMessengerEntry => entry !== null)
+}
+
+function resolveLegacySingleMessenger(settings: SupportMessengerSettings): SupportMessengerEntry[] {
+ const messenger = (settings.messenger ?? '').trim().toLowerCase()
+ const id = (settings.messengerId ?? '').trim()
+ if (!messenger || !id) return []
+ if (!VALID_SUPPORT_MESSENGERS.has(messenger)) return []
+ return [{ messenger, id }]
+}
+
+export async function openSupportMessengerEntry(entry: ResolvedSupportMessengerEntry | SupportMessengerEntry) {
+ const url = 'url' in entry && entry.url
+ ? entry.url
+ : buildSupportMessengerUrl(entry.messenger, entry.id)
+ if (!url) return false
+ window.open(url, '_blank', 'noopener,noreferrer')
+ return true
+}
+
export async function loadSupportMessengerFromSettings() {
try {
const settings = await getSettings()
if (!settings || !Array.isArray(settings)) return
supportMessengerSettings.value = {
+ messengers: resolveSupportMessengersFromSettings(settings),
messenger: getSupportSettingValue(settings, 'support_messenger'),
messengerId: getSupportSettingValue(settings, 'support_messenger_id'),
messengerText: getSupportSettingValue(settings, 'support_messenger_text'),
diff --git a/src/views/base/fund/info/composables/useFundInfoPage.ts b/src/views/base/fund/info/composables/useFundInfoPage.ts
index bf8c01e..fea50ec 100644
--- a/src/views/base/fund/info/composables/useFundInfoPage.ts
+++ b/src/views/base/fund/info/composables/useFundInfoPage.ts
@@ -39,7 +39,10 @@ export function useFundInfoPage() {
try {
loading.value = true
items.value = []
- Object.assign(pagination.value, { wallet_id: info.value.wallet.id })
+ Object.assign(pagination.value, {
+ wallet_id: info.value.wallet.id,
+ product_id: infoId.value ?? undefined,
+ })
tableOutput.value = await getFundTransactionTable(pagination.value)
items.value.push(...(tableOutput.value?.rows ?? []))
} catch (e) {
diff --git a/src/views/base/home/desktop/components/DesktopAssetsMetrics.vue b/src/views/base/home/desktop/components/DesktopAssetsMetrics.vue
index 02c9827..8294b29 100644
--- a/src/views/base/home/desktop/components/DesktopAssetsMetrics.vue
+++ b/src/views/base/home/desktop/components/DesktopAssetsMetrics.vue
@@ -62,7 +62,7 @@ const metrics = computed(() => {
label: metal.label,
value: formatMetalTotal(metal),
metal,
- showInfo: index === 0,
+ showInfo: true,
onInfo: 'gold',
})
})