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', }) })