diff --git a/src/config/defaultKhanegiUiConfig.ts b/src/config/defaultKhanegiUiConfig.ts index 20fa628..37ad871 100644 --- a/src/config/defaultKhanegiUiConfig.ts +++ b/src/config/defaultKhanegiUiConfig.ts @@ -91,6 +91,7 @@ export function createDefaultKhanegiUiConfig() { { id: 'user', label: 'تنظیمات', icon: 'vuesax-linear:setting-2', route: 'user', order: 8, visible: true }, ], bottom: [ + // ۴ جایگاه ثابت نوار پایین — با get-user/khanegi-nav-sample.ts یکی بماند { id: 'gold', label: 'خرید و فروش', icon: 'vuesax-linear:coin', route: 'gold', order: 1, visible: true }, { id: 'transactions', label: 'تراکنش‌ها', icon: 'vuesax-linear:receipt-item', route: 'transactions', order: 2, visible: true }, { id: 'wallet', label: 'کیف پول', icon: 'vuesax-linear:wallet-add', route: 'wallet', order: 3, visible: true }, @@ -118,6 +119,88 @@ export function createDefaultKhanegiUiConfig() { export type UiConfigBase = ReturnType +/** نوار پایین موبایل فقط ۴ جایگاه دارد (خانه جدا در FAB وسط است) */ +export const MAX_BOTTOM_NAV_SLOTS = 4 + +export type KhanegiNavItem = UiConfigBase['app']['navigation']['sidebar'][number] + +function cloneJson(value: T): T { + return JSON.parse(JSON.stringify(value)) as T +} + +export function getKhanegiNavSample() { + const { navigation } = createDefaultKhanegiUiConfig().app + return { + sidebar: cloneJson(navigation.sidebar), + bottom: cloneJson(navigation.bottom), + sidebarReferral: cloneJson(navigation.sidebarReferral), + } +} + +/** آیتم‌های سمپل را نگه می‌دارد؛ لیبل/وضعیت نسخه‌ها روی همان id overlay می‌شود */ +export function mergeNavItemsFromSample( + sample: KhanegiNavItem[], + stored: KhanegiNavItem[] | undefined, + maxVisible?: number, +): KhanegiNavItem[] { + const sampleById = new Map(sample.map((item) => [item.id, item])) + const storedList = Array.isArray(stored) ? stored : [] + const kept: KhanegiNavItem[] = [] + const seen = new Set() + + for (const prev of storedList) { + const base = sampleById.get(prev.id) + if (!base || seen.has(prev.id)) continue + seen.add(prev.id) + kept.push({ + ...base, + label: prev.label || base.label, + icon: prev.icon || base.icon, + visible: typeof prev.visible === 'boolean' ? prev.visible : base.visible, + query: prev.query ?? base.query, + }) + } + + for (const item of sample) { + if (seen.has(item.id)) continue + kept.push({ ...item }) + } + + if (typeof maxVisible === 'number') { + let visibleCount = 0 + for (const item of kept) { + if (!item.visible) continue + visibleCount += 1 + if (visibleCount > maxVisible) item.visible = false + } + } + + return kept.map((item, index) => ({ ...item, order: index + 1 })) +} + +export function applyKhanegiNavSample(app: { navigation?: any } | null | undefined) { + if (!app || typeof app !== 'object') return + const sample = getKhanegiNavSample() + if (!app.navigation || typeof app.navigation !== 'object') { + app.navigation = { + type: 'both', + sidebar: sample.sidebar, + bottom: sample.bottom, + sidebarReferral: sample.sidebarReferral, + } + return + } + app.navigation.sidebar = mergeNavItemsFromSample(sample.sidebar, app.navigation.sidebar) + app.navigation.bottom = mergeNavItemsFromSample( + sample.bottom.slice(0, MAX_BOTTOM_NAV_SLOTS), + app.navigation.bottom, + MAX_BOTTOM_NAV_SLOTS, + ).slice(0, MAX_BOTTOM_NAV_SLOTS) + if (!app.navigation.sidebarReferral || typeof app.navigation.sidebarReferral !== 'object') { + app.navigation.sidebarReferral = sample.sidebarReferral + } +} + export type UiConfig = UiConfigBase & { /** Immutable factory look — set once; never overwrite on normal save */ defaultSnapshot?: { @@ -173,6 +256,9 @@ export function normalizeKhanegiUiConfigForClient> if (app.navigation.sidebarReferral?.route === 'products') { app.navigation.sidebarReferral.route = 'buyProduct' } + applyKhanegiNavSample(app) + } else { + applyKhanegiNavSample(app) } } diff --git a/src/pages/UiConfigPage.vue b/src/pages/UiConfigPage.vue index 6b81528..1f648b5 100644 --- a/src/pages/UiConfigPage.vue +++ b/src/pages/UiConfigPage.vue @@ -2,12 +2,14 @@ import { computed, onMounted, reactive, ref, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' import { message, Modal } from 'ant-design-vue' -import { ArrowRightOutlined } from '@ant-design/icons-vue' +import { ArrowRightOutlined, HolderOutlined } from '@ant-design/icons-vue' import api from '@/api/http' import VersionPageTabs from '@/components/VersionPageTabs.vue' import { + applyKhanegiNavSample, cloneKhanegiAppSnapshot, createDefaultKhanegiUiConfig, + MAX_BOTTOM_NAV_SLOTS, normalizeKhanegiUiConfigForClient, } from '@/config/defaultKhanegiUiConfig' import { @@ -37,6 +39,7 @@ const advancedJson = ref('') const jsonError = ref('') const selectedPreset = ref('navy-gold') const previewMode = ref<'dark' | 'light'>('dark') +const bottomDragFrom = ref(null) const config = reactive(createDefaultKhanegiUiConfig()) as ReturnType< typeof createDefaultKhanegiUiConfig @@ -44,6 +47,57 @@ const config = reactive(createDefaultKhanegiUiConfig()) as ReturnType< defaultSnapshot?: ReturnType } +function syncBottomNavOrder() { + config.app.navigation.bottom.forEach((item, index) => { + item.order = index + 1 + }) +} + +function applySharedNavSample() { + applyKhanegiNavSample(config.app) + if (config.defaultSnapshot?.app) applyKhanegiNavSample(config.defaultSnapshot.app) + syncBottomNavOrder() +} + +const visibleBottomCount = computed( + () => config.app.navigation.bottom.filter((item) => item.visible).length, +) + +function onBottomVisibleChange(item: { visible: boolean }, checked: boolean) { + if (checked && visibleBottomCount.value >= MAX_BOTTOM_NAV_SLOTS && !item.visible) { + message.warning('منوی پایین فقط ۴ جایگاه دارد') + return + } + item.visible = checked +} + +function onBottomDragStart(index: number, e: DragEvent) { + bottomDragFrom.value = index + e.dataTransfer?.setData('text/plain', String(index)) + if (e.dataTransfer) e.dataTransfer.effectAllowed = 'move' +} + +function onBottomDragOver(index: number, e: DragEvent) { + e.preventDefault() + const from = bottomDragFrom.value + if (from == null || from === index) return + const list = config.app.navigation.bottom + const [moved] = list.splice(from, 1) + list.splice(index, 0, moved) + bottomDragFrom.value = index + syncBottomNavOrder() +} + +function onBottomDrop() { + bottomDragFrom.value = null + syncBottomNavOrder() +} + +function onBottomDragEnd() { + bottomDragFrom.value = null + syncBottomNavOrder() +} + const hasDefaultSnapshot = computed(() => Boolean(config.defaultSnapshot)) const isCustom = computed(() => selectedPreset.value === 'custom') @@ -129,6 +183,7 @@ function applyAdvancedJson() { if (keepSnapshot && !config.defaultSnapshot) { config.defaultSnapshot = keepSnapshot } + applySharedNavSample() ensureSchemes() selectedPreset.value = resolvePresetId(config) jsonError.value = '' @@ -180,6 +235,7 @@ async function load() { Object.assign(config, createDefaultKhanegiUiConfig()) } } + applySharedNavSample() ensureSchemes() if (!config.defaultSnapshot) { config.defaultSnapshot = cloneKhanegiAppSnapshot(config) @@ -285,6 +341,7 @@ function resetToDefault() { const snap = config.defaultSnapshot! config.app = JSON.parse(JSON.stringify(snap.app)) config.pages = JSON.parse(JSON.stringify(snap.pages || {})) + applySharedNavSample() ensureSchemes() selectedPreset.value = resolvePresetId(config) previewMode.value = config.app.theme.mode === 'light' ? 'light' : 'dark' @@ -523,8 +580,30 @@ onMounted(load)

منوی پایین موبایل

-