diff --git a/src/app/setupRouter.ts b/src/app/setupRouter.ts index 87e2699..96b597d 100644 --- a/src/app/setupRouter.ts +++ b/src/app/setupRouter.ts @@ -77,6 +77,17 @@ export function setupRouterGuards() { } } + if (to.name === 'giftCard' && getToken()) { + const { useSettingsStore } = await import('@/store/settingsStore') + const settingsStore = useSettingsStore() + if (!settingsStore.loaded) { + await settingsStore.fetchSettings() + } + if (!settingsStore.isSettingTruthy('has_page_gift_card')) { + return next({ name: (from.name as string) || 'user' }) + } + } + if (isFirst) { isFirst = false diff --git a/src/components/BasicPacks/BasicInput.vue b/src/components/BasicPacks/BasicInput.vue index cf17223..0e91554 100644 --- a/src/components/BasicPacks/BasicInput.vue +++ b/src/components/BasicPacks/BasicInput.vue @@ -14,6 +14,7 @@ :type="props.type" class="" @change="handlerChange" + @input="handlerChange" /> @@ -36,7 +38,12 @@ import {usePrefix} from "@/utils/usePrefix"; import {computed} from "vue"; import {usePrice} from "@/components/BasicPacks/BasicPriceInput/src/utils/usePrice"; -import {persianToEnglishDigits, shouldNormalizeInputDigits} from '@/utils/persianDigits'; +import { + normalizeDecimalInput, + persianToEnglishDigits, + shouldNormalizeDecimalSeparators, + shouldNormalizeInputDigits, +} from '@/utils/persianDigits'; //Props interface Prop { @@ -79,18 +86,27 @@ function normalizeValue(value: unknown) { return value } + if (shouldNormalizeDecimalSeparators(props)) { + const isPrice = String(props.type ?? '').trim().toLowerCase() === 'price' + return normalizeDecimalInput(value as string | number, { + commaAsThousand: isPrice, + }) + } + let normalized = String(value) if (shouldNormalizeInputDigits(props)) { normalized = persianToEnglishDigits(normalized) } - if (props.type === 'price') { - normalized = normalized.replaceAll(',', '') - } return normalized } -const handlerChange = (e: any) => { - const normalized = normalizeValue(e.target.value) +const handlerChange = (e: Event | string) => { + const target = typeof e === 'string' ? null : (e.target as HTMLInputElement | null) + const raw = typeof e === 'string' ? e : target?.value + const normalized = normalizeValue(raw) + if (target && typeof normalized === 'string' && target.value !== normalized) { + target.value = normalized + } emits('update:value', normalized) emits('onchange', normalized) } diff --git a/src/components/BasicPacks/BasicPriceInput/src/BasicPriceInput.vue b/src/components/BasicPacks/BasicPriceInput/src/BasicPriceInput.vue index 2c7bb55..fdf4d6b 100644 --- a/src/components/BasicPacks/BasicPriceInput/src/BasicPriceInput.vue +++ b/src/components/BasicPacks/BasicPriceInput/src/BasicPriceInput.vue @@ -10,7 +10,7 @@ import {usePrice} from './utils/usePrice' import {computed} from "vue"; import BasicInput from "../../BasicInput.vue"; -import {persianToEnglishDigits} from '@/utils/persianDigits' +import {normalizeDecimalInput} from '@/utils/persianDigits' interface Props { value?: string | number @@ -24,7 +24,7 @@ const props = withDefaults(defineProps(), {}) const _val = computed({ set(val: string) { - emit('update:value', persianToEnglishDigits(val).replaceAll(',', '')) + emit('update:value', normalizeDecimalInput(val)) }, get() { return props.value ? usePrice(props.value as number, true, '').trim() : '' diff --git a/src/composables/useSupportMessenger.ts b/src/composables/useSupportMessenger.ts index ed7e53e..7723e20 100644 --- a/src/composables/useSupportMessenger.ts +++ b/src/composables/useSupportMessenger.ts @@ -7,7 +7,6 @@ import { loadSupportMessengerFromSettings, messengerNeedsClipboardCopy, openSupportMessengerEntry, - openSupportMessengerWithMessage, resolveDepositGuideText, resolveDepositSupportMessage, resolveSupportMessengerEntries, @@ -138,23 +137,22 @@ export function useSupportMessenger() { const messengerEntries = supportMessengerEntries.value const entries = pickerEntries.value + // لیست خالی + چت خاموش → هیچ‌کار + if (entries.length === 0) { + return false + } + + // فقط چت (لیست پیام‌رسان خالی) → مستقیم چت if (isChatEnabled.value && messengerEntries.length === 0) { return goToSupportChat() } - if (entries.length === 0) { - return openSupportMessengerWithMessage( - supportMessengerSettings.value.messenger, - supportMessengerSettings.value.messengerId, - draft, - legacySupportLink.value, - ) - } - + // فقط یک گزینه (چت یا یک پیام‌رسان) → مستقیم همان if (entries.length === 1) { return openMessengerEntry(entries[0], draft) } + // چند گزینه (چت + لینک‌ها یا چند پیام‌رسان) → منوی انتخاب const now = Date.now() if (supportPickerOpen.value) { if (now - supportPickerOpenedAt < 450) return true @@ -249,7 +247,7 @@ export function useSupportMessenger() { } const hasMessengerLink = computed( - () => isChatEnabled.value || supportMessengerEntries.value.length > 0 || !!messengerLink.value, + () => isChatEnabled.value || supportMessengerEntries.value.length > 0, ) const hasMultipleMessengers = computed(() => pickerEntries.value.length > 1) const hasSupportPhoneLink = computed(() => !!supportPhoneLink.value) diff --git a/src/layouts/desktop/DesktopSidebar.vue b/src/layouts/desktop/DesktopSidebar.vue index 5d6a778..bb808b1 100644 --- a/src/layouts/desktop/DesktopSidebar.vue +++ b/src/layouts/desktop/DesktopSidebar.vue @@ -64,6 +64,9 @@ const secondaryItems = computed(() => if (item.id === 'notifyTrigger') { return settingsStore.isSettingTruthy('has_page_price_trigger') } + if (item.id === 'giftCard') { + return settingsStore.isSettingTruthy('has_page_gift_card') + } return true }), ) diff --git a/src/layouts/mobile/MobileOverflowMenu.vue b/src/layouts/mobile/MobileOverflowMenu.vue index ff03193..1734193 100644 --- a/src/layouts/mobile/MobileOverflowMenu.vue +++ b/src/layouts/mobile/MobileOverflowMenu.vue @@ -71,7 +71,7 @@ settingsStore.isSettingTruthy('has_page_price_trigger'), ) +const giftCardEnabled = computed(() => + settingsStore.isSettingTruthy('has_page_gift_card'), +) function go(routeName: string) { emit('navigate', routeName) diff --git a/src/utils/persianDigits.ts b/src/utils/persianDigits.ts index fc8976f..32c8884 100644 --- a/src/utils/persianDigits.ts +++ b/src/utils/persianDigits.ts @@ -13,6 +13,49 @@ export function persianToEnglishDigits(value: string | number | null | undefined .replace(/٫/g, '.') } +/** + * اعداد فارسی/عربی + جداکننده‌های اعشار رایج ماشین‌حساب فارسی. + * - `commaAsThousand` true (مبلغ): `,` / `،` حذف می‌شوند + * - `commaAsThousand` false (وزن/اعشار): `,` / `،` هم → `.` + */ +export function normalizeDecimalInput( + val: string | number | null | undefined, + options?: { commaAsThousand?: boolean } +): string { + if (val == null || val === '') return '' + + const commaAsThousand = options?.commaAsThousand ?? true + let normalized = persianToEnglishDigits(String(val)) + + if (commaAsThousand) { + normalized = normalized.replace(/[,،]/g, '') + } else { + normalized = normalized.replace(/[,،]/g, '.') + } + + normalized = normalized + .replace(/\//g, '.') + .replace(/[^\d.]/g, '') + + const firstDot = normalized.indexOf('.') + if (firstDot !== -1) { + normalized = + normalized.slice(0, firstDot + 1) + + normalized.slice(firstDot + 1).replace(/\./g, '') + } + + return normalized +} + +export function parseDecimalInput( + val: string | number | null | undefined, + options?: { commaAsThousand?: boolean } +): number { + const normalized = normalizeDecimalInput(val, options) + if (!normalized) return NaN + return Number(normalized) +} + export function shouldNormalizeInputDigits(options: { isPassword?: boolean type?: string @@ -27,3 +70,16 @@ export function shouldNormalizeInputDigits(options: { const mode = String(options.inputmode ?? '').trim().toLowerCase() return mode === 'numeric' || mode === 'decimal' || mode === 'number' } + +/** فیلدهای وزن/مبلغ/عدد اعشاری که باید `/` را ممیز بگیرند */ +export function shouldNormalizeDecimalSeparators(options: { + isPassword?: boolean + type?: string + inputmode?: string +}): boolean { + if (options.isPassword) return false + const type = String(options.type ?? '').trim().toLowerCase() + if (type === 'price' || type === 'number') return true + const mode = String(options.inputmode ?? '').trim().toLowerCase() + return mode === 'numeric' || mode === 'decimal' || mode === 'number' +} diff --git a/src/utils/supportMessenger.ts b/src/utils/supportMessenger.ts index f5dca50..909ad97 100644 --- a/src/utils/supportMessenger.ts +++ b/src/utils/supportMessenger.ts @@ -87,8 +87,11 @@ export function parseSupportMessengersJson(raw?: string | null): SupportMessenge export function resolveSupportMessengersFromSettings( settings: Array<{ key?: string; value?: unknown }>, ): SupportMessengerEntry[] { - const fromJson = parseSupportMessengersJson(getSupportSettingValue(settings, 'support_messengers')) - if (fromJson.length > 0) return fromJson + // اگر کلید جدید در تنظیمات هست (حتی خالی/null)، فقط همان لیست معتبر است — legacy بله را برنگردان + const hasMessengersKey = settings.some((row) => row?.key === 'support_messengers') + if (hasMessengersKey) { + return parseSupportMessengersJson(getSupportSettingValue(settings, 'support_messengers')) + } const legacyMessenger = getSupportSettingValue(settings, 'support_messenger').toLowerCase() const legacyId = getSupportSettingValue(settings, 'support_messenger_id') @@ -107,9 +110,8 @@ export function resolveSupportMessengerEntries( ): ResolvedSupportMessengerEntry[] { const resolved = settings ?? supportMessengerSettings.value const text = (messageText ?? '').trim() - const source = resolved.messengers?.length - ? resolved.messengers - : resolveLegacySingleMessenger(resolved) + // فقط لیست resolve‌شده؛ قبل از load خالی است تا legacy دوباره دکمه نسازد + const source = Array.isArray(resolved.messengers) ? resolved.messengers : [] return source .map((entry) => { @@ -125,14 +127,6 @@ export function resolveSupportMessengerEntries( .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 }] -} - /** @deprecated از depositGuideText استفاده کنید */ export const DEFAULT_SUPPORT_MESSENGER_TEXT = DEFAULT_DEPOSIT_GUIDE_TEXT diff --git a/src/utils/useAxios.ts b/src/utils/useAxios.ts index 012b0bf..197f27d 100644 --- a/src/utils/useAxios.ts +++ b/src/utils/useAxios.ts @@ -71,29 +71,46 @@ _axios.interceptors.response.use( } const apiMessage = extractApiErrorMessage(response?.data) + const status = Number(response?.status ?? 0) if (response?.status === 403) { - showApiError(apiMessage || 'شما دسترسی لازم برای انجام این عملیات را ندارید', config) + showApiError( + withHttpStatus(apiMessage || 'شما دسترسی لازم برای انجام این عملیات را ندارید', 403), + config, + ) return Promise.reject(error) } if (response?.status === 422) { if (apiMessage) { - showApiError(apiMessage, config) + showApiError(withHttpStatus(apiMessage, 422), config) + } else { + showApiError(withHttpStatus('خطا در اعتبارسنجی', 422), config) } return Promise.reject(error) } if (response) { - showApiError(apiMessage || DEFAULT_LOAD_ERROR, config) + if (status >= 500) { + showApiError(withHttpStatus(SERVER_ERROR, status), config) + } else { + showApiError(withHttpStatus(apiMessage || DEFAULT_LOAD_ERROR, status), config) + } return Promise.reject(error) } - showApiError(DEFAULT_LOAD_ERROR, config) + // بدون status (نه 4xx نه 5xx): قطع شبکه، timeout، DNS، … + showApiError(NETWORK_ERROR, config) return Promise.reject(error) }) const DEFAULT_LOAD_ERROR = 'خطا در بارگذاری اطلاعات' +const SERVER_ERROR = 'خطا در برقراری ارتباط با سرور' +const NETWORK_ERROR = 'لطفا اینترنت خود را بررسی کنید و مجددا تلاش کنید' + +function withHttpStatus(message: string, status: number): string { + return `${message} (${status})` +} function extractApiErrorMessage(data: unknown): string { if (!data || typeof data !== 'object') { diff --git a/src/views/base/home/desktop/styles/desktop-home.less b/src/views/base/home/desktop/styles/desktop-home.less index 8d18032..72d8973 100644 --- a/src/views/base/home/desktop/styles/desktop-home.less +++ b/src/views/base/home/desktop/styles/desktop-home.less @@ -477,7 +477,7 @@ .dhome-default-actions { display: grid; - grid-template-columns: repeat(5, minmax(0, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(96px, 1fr)); gap: 8px; } diff --git a/src/views/base/home/index.vue b/src/views/base/home/index.vue index 85e40a4..e891e39 100644 --- a/src/views/base/home/index.vue +++ b/src/views/base/home/index.vue @@ -67,87 +67,68 @@
-
+
-
- +
+
-
خرید و فروش
+
خرید و فروش
-
- +
+
-
واریز نقدی
+
واریز نقدی
-
- +
+
-
موجودی
+
موجودی
-
- +
+
-
دارایی ها
+
دارایی ها
-
- +
+
-
قلک
+
قلک
-
- +
+
-
کسب درآمد
+
کسب درآمد
-
@@ -957,6 +938,60 @@ function copyCode() { } } +.home-quick-actions { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 2px; +} + +.home-quick-action { + flex: 1 1 0; + min-width: 0; + text-align: center; +} + +.home-quick-action__icon { + width: 40px; + height: 40px; + margin: 0 auto; + display: flex; + align-items: center; + justify-content: center; + border-radius: 9999px; + background: var(--App-primary, #a72b77); + border: solid 1px #a72b77; +} + +.home-quick-action__label { + margin-top: 4px; + padding: 0 2px; + color: var(--App-primary, #a72b77); + font-size: 10px; + font-weight: 500; + line-height: 1.25; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + word-break: break-word; +} + +@media (min-width: 640px) { + .home-quick-actions { + gap: 4px; + } + + .home-quick-action__icon { + width: 44px; + height: 44px; + } + + .home-quick-action__label { + font-size: 11px; + } +} + .odometer { font-size: 60px; text-align: center; diff --git a/src/views/base/piggy/index.vue b/src/views/base/piggy/index.vue index 414775b..b6be005 100644 --- a/src/views/base/piggy/index.vue +++ b/src/views/base/piggy/index.vue @@ -1,95 +1,97 @@ @@ -143,23 +145,33 @@ onMounted(async () => { diff --git a/src/views/base/piggy/info.vue b/src/views/base/piggy/info.vue index 79f0637..acc8b00 100644 --- a/src/views/base/piggy/info.vue +++ b/src/views/base/piggy/info.vue @@ -1,75 +1,77 @@