feat(ui): selectable bottom-nav slots and sidebar drag reorder

Let each of the four bottom slots pick a menu from the shared catalog, and allow reordering the sidebar the same way.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-24 12:14:55 +03:30
co-authored by Cursor
parent 238fb7966c
commit 72b0a819af
2 changed files with 177 additions and 11 deletions
+118 -6
View File
@@ -9,8 +9,10 @@ import {
applyKhanegiNavSample,
cloneKhanegiAppSnapshot,
createDefaultKhanegiUiConfig,
getKhanegiBottomNavChoices,
MAX_BOTTOM_NAV_SLOTS,
normalizeKhanegiUiConfigForClient,
type KhanegiNavItem,
} from '@/config/defaultKhanegiUiConfig'
import {
applyKhanegiPreset,
@@ -40,6 +42,7 @@ const jsonError = ref('')
const selectedPreset = ref<KhanegiPresetId>('navy-gold')
const previewMode = ref<'dark' | 'light'>('dark')
const bottomDragFrom = ref<number | null>(null)
const sidebarDragFrom = ref<number | null>(null)
const config = reactive(createDefaultKhanegiUiConfig()) as ReturnType<
typeof createDefaultKhanegiUiConfig
@@ -53,16 +56,62 @@ function syncBottomNavOrder() {
})
}
function syncSidebarNavOrder() {
config.app.navigation.sidebar.forEach((item, index) => {
item.order = index + 1
})
}
function applySharedNavSample() {
applyKhanegiNavSample(config.app)
if (config.defaultSnapshot?.app) applyKhanegiNavSample(config.defaultSnapshot.app)
syncSidebarNavOrder()
syncBottomNavOrder()
}
const bottomNavChoices = computed(() => getKhanegiBottomNavChoices())
const visibleBottomCount = computed(
() => config.app.navigation.bottom.filter((item) => item.visible).length,
)
function bottomSlotOptions(slotIndex: number) {
const used = new Set(
config.app.navigation.bottom
.map((item, i) => (i === slotIndex ? null : item.id))
.filter(Boolean) as string[],
)
return bottomNavChoices.value.map((choice) => ({
value: choice.id,
label: `${choice.label} (${choice.route})`,
disabled: used.has(choice.id),
}))
}
function onBottomSlotMenuChange(slotIndex: number, choiceId: string) {
const choice = bottomNavChoices.value.find((item) => item.id === choiceId)
if (!choice) return
const list = config.app.navigation.bottom
const current = list[slotIndex]
if (!current || current.id === choiceId) return
const otherIndex = list.findIndex((item, i) => i !== slotIndex && item.id === choiceId)
if (otherIndex >= 0) {
list[otherIndex] = {
...current,
order: otherIndex + 1,
} as KhanegiNavItem
}
list[slotIndex] = {
...choice,
label: choice.label,
visible: current.visible,
order: slotIndex + 1,
} as KhanegiNavItem
syncBottomNavOrder()
}
function onBottomVisibleChange(item: { visible: boolean }, checked: boolean) {
if (checked && visibleBottomCount.value >= MAX_BOTTOM_NAV_SLOTS && !item.visible) {
message.warning('منوی پایین فقط ۴ جایگاه دارد')
@@ -98,6 +147,33 @@ function onBottomDragEnd() {
syncBottomNavOrder()
}
function onSidebarDragStart(index: number, e: DragEvent) {
sidebarDragFrom.value = index
e.dataTransfer?.setData('text/plain', String(index))
if (e.dataTransfer) e.dataTransfer.effectAllowed = 'move'
}
function onSidebarDragOver(index: number, e: DragEvent) {
e.preventDefault()
const from = sidebarDragFrom.value
if (from == null || from === index) return
const list = config.app.navigation.sidebar
const [moved] = list.splice(from, 1)
list.splice(index, 0, moved)
sidebarDragFrom.value = index
syncSidebarNavOrder()
}
function onSidebarDrop() {
sidebarDragFrom.value = null
syncSidebarNavOrder()
}
function onSidebarDragEnd() {
sidebarDragFrom.value = null
syncSidebarNavOrder()
}
const hasDefaultSnapshot = computed(() => Boolean(config.defaultSnapshot))
const isCustom = computed(() => selectedPreset.value === 'custom')
@@ -572,7 +648,26 @@ onMounted(load)
<a-tab-pane key="nav" tab="منوها">
<section class="form-section">
<h3>منوی کناری</h3>
<div v-for="item in config.app.navigation.sidebar" :key="item.id" class="nav-row">
<p class="page-sub" style="margin-bottom: 12px">
با دستگیره ترتیب آیتم‌های سایدبار را عوض کنید.
</p>
<div
v-for="(item, index) in config.app.navigation.sidebar"
:key="item.id"
class="nav-row sidebar-row"
:class="{ 'is-dragging': sidebarDragFrom === index }"
@dragover.prevent="onSidebarDragOver(index, $event)"
@drop.prevent="onSidebarDrop"
>
<span
class="drag-handle"
draggable="true"
title="جابه‌جایی"
@dragstart="onSidebarDragStart(index, $event)"
@dragend="onSidebarDragEnd"
>
<HolderOutlined />
</span>
<a-switch v-model:checked="item.visible" />
<a-input v-model:value="item.label" />
<span class="nav-route" dir="ltr">{{ item.route }}</span>
@@ -581,12 +676,12 @@ onMounted(load)
<section class="form-section">
<h3>منوی پایین موبایل</h3>
<p class="page-sub" style="margin-bottom: 12px">
۴ جایگاه ثابت — بیشتر در نوار پایین جا نمی‌شود. با دستگیره، ترتیب را عوض کنید.
۴ جایگاه ثابت — برای هر جایگاه از سلکت انتخاب کنید کدام منو باشد. با دستگیره ترتیب را عوض کنید.
</p>
<div
v-for="(item, index) in config.app.navigation.bottom"
:key="item.id"
class="nav-row"
:key="`${index}-${item.id}`"
class="nav-row bottom-slot-row"
:class="{ 'is-dragging': bottomDragFrom === index }"
@dragover.prevent="onBottomDragOver(index, $event)"
@drop.prevent="onBottomDrop"
@@ -604,8 +699,13 @@ onMounted(load)
:checked="item.visible"
@change="(v: boolean) => onBottomVisibleChange(item, v)"
/>
<a-input v-model:value="item.label" />
<span class="nav-route" dir="ltr">{{ item.route }}</span>
<a-select
:value="item.id"
class="bottom-slot-select"
:options="bottomSlotOptions(index)"
@change="(v: string) => onBottomSlotMenuChange(index, v)"
/>
<a-input v-model:value="item.label" placeholder="برچسب نمایش" />
</div>
</section>
<section class="form-section">
@@ -892,6 +992,18 @@ onMounted(load)
margin-bottom: 10px;
}
.sidebar-row {
grid-template-columns: auto auto 1fr auto;
}
.bottom-slot-row {
grid-template-columns: auto auto minmax(160px, 1.1fr) minmax(120px, 1fr);
}
.bottom-slot-select {
width: 100%;
}
.nav-row.is-dragging {
opacity: 0.55;
}