Template
first commit
This commit is contained in:
@@ -0,0 +1,410 @@
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<v-row>
|
||||
<!-- Information Panel - Responsive -->
|
||||
<v-col cols="12" lg="6" xl="5">
|
||||
<v-card elevation="2" class="h-100">
|
||||
<v-card-title class="bg-primary text-white">
|
||||
<v-icon left>mdi-information</v-icon>
|
||||
INFORMATION
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-4">
|
||||
<v-form ref="formRef" v-model="isFormValid">
|
||||
<!-- Title Menu -->
|
||||
<v-select
|
||||
v-model="menuForm.titleMenu"
|
||||
:items="menuOptions"
|
||||
label="Title Menu"
|
||||
variant="outlined"
|
||||
class="mb-3"
|
||||
prepend-inner-icon="mdi-format-title"
|
||||
:rules="[rules.required]"
|
||||
/>
|
||||
|
||||
<!-- Side Menu -->
|
||||
<v-select
|
||||
v-model="menuForm.sideMenu"
|
||||
:items="menuOptions"
|
||||
label="Side Menu"
|
||||
variant="outlined"
|
||||
class="mb-3"
|
||||
prepend-inner-icon="mdi-menu"
|
||||
:rules="[rules.required]"
|
||||
/>
|
||||
|
||||
<!-- Name Menu -->
|
||||
<v-text-field
|
||||
v-model="menuForm.nameMenu"
|
||||
label="Name Menu"
|
||||
variant="outlined"
|
||||
class="mb-3"
|
||||
prepend-inner-icon="mdi-tag"
|
||||
:rules="[rules.required]"
|
||||
clearable
|
||||
/>
|
||||
|
||||
<!-- Link URL Menu -->
|
||||
<v-text-field
|
||||
v-model="menuForm.linkUrlMenu"
|
||||
label="Link URL Menu"
|
||||
variant="outlined"
|
||||
class="mb-3"
|
||||
prepend-inner-icon="mdi-link"
|
||||
placeholder="/example/path"
|
||||
clearable
|
||||
/>
|
||||
|
||||
<!-- Icon Menu -->
|
||||
<v-text-field
|
||||
v-model="menuForm.iconMenu"
|
||||
label="Icon Menu"
|
||||
variant="outlined"
|
||||
class="mb-4"
|
||||
prepend-inner-icon="mdi-emoticon"
|
||||
placeholder="mdi-home"
|
||||
clearable
|
||||
>
|
||||
<template #append-inner>
|
||||
<v-icon
|
||||
v-if="menuForm.iconMenu"
|
||||
color="primary"
|
||||
>
|
||||
{{ menuForm.iconMenu }}
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-form>
|
||||
|
||||
<!-- Add Menu Button -->
|
||||
<v-btn
|
||||
color="teal"
|
||||
variant="elevated"
|
||||
@click="addMenuItem"
|
||||
:disabled="!isFormValid"
|
||||
:loading="isAdding"
|
||||
block
|
||||
size="large"
|
||||
class="mb-2"
|
||||
>
|
||||
<v-icon left>mdi-plus</v-icon>
|
||||
Add Menu
|
||||
</v-btn>
|
||||
|
||||
<!-- Clear Form Button -->
|
||||
<v-btn
|
||||
color="grey-darken-1"
|
||||
variant="outlined"
|
||||
@click="clearForm"
|
||||
block
|
||||
>
|
||||
<v-icon left>mdi-refresh</v-icon>
|
||||
Clear Form
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<!-- Menu Management Panel - Responsive -->
|
||||
<v-col cols="12" lg="6" xl="7">
|
||||
<v-card elevation="2" class="h-100">
|
||||
<v-card-title class="bg-blue text-white d-flex justify-space-between align-center">
|
||||
<div>
|
||||
<v-icon left>mdi-menu-open</v-icon>
|
||||
CREATE CUSTOM MENU
|
||||
</div>
|
||||
<v-chip color="white" text-color="blue" size="small">
|
||||
{{ menuItems.length }} items
|
||||
</v-chip>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-4">
|
||||
<!-- Action Bar -->
|
||||
<v-row class="mb-4" align="center">
|
||||
<v-col cols="12" sm="6">
|
||||
<v-select
|
||||
v-model="selectedReference"
|
||||
:items="references"
|
||||
label="Reference"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
prepend-inner-icon="mdi-database"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" class="d-flex justify-end">
|
||||
<v-btn
|
||||
color="teal"
|
||||
variant="elevated"
|
||||
@click="reloadMenu"
|
||||
:loading="isLoading"
|
||||
class="me-2"
|
||||
>
|
||||
<v-icon left>mdi-refresh</v-icon>
|
||||
<span class="d-none d-sm-inline">Reload</span>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="success"
|
||||
variant="elevated"
|
||||
@click="saveAllMenus"
|
||||
:loading="isSaving"
|
||||
>
|
||||
<v-icon left>mdi-content-save</v-icon>
|
||||
<span class="d-none d-sm-inline">Save</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Instructions -->
|
||||
<v-alert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
density="compact"
|
||||
>
|
||||
<v-icon>mdi-drag</v-icon>
|
||||
Drag The Menu List To Re-Order, Click Update Menu To Save The Position, To Add Item On Menu, Use Form Below
|
||||
</v-alert>
|
||||
|
||||
<!-- Menu Tree Header -->
|
||||
<div class="menu-header bg-blue text-white pa-2 rounded-t">
|
||||
<v-row no-gutters>
|
||||
<v-col cols="6" sm="4">
|
||||
<strong>MENU TITLE</strong>
|
||||
</v-col>
|
||||
<v-col cols="4" sm="5" class="d-none d-sm-block">
|
||||
<strong>URL LINK</strong>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="3" class="text-right">
|
||||
<strong>ACTION</strong>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
|
||||
<!-- Menu Tree -->
|
||||
<div class="menu-tree-container" style="max-height: 400px; overflow-y: auto;">
|
||||
<MenuTreeItem
|
||||
v-for="item in menuItems"
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
:level="0"
|
||||
@edit="editMenuItem"
|
||||
@delete="deleteMenuItem"
|
||||
@toggle="toggleMenuItem"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="menuItems.length === 0" class="text-center pa-8">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-menu-off</v-icon>
|
||||
<p class="text-grey-darken-1 mt-2">No menu items created yet</p>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Snackbar for notifications -->
|
||||
<v-snackbar
|
||||
v-model="snackbar.show"
|
||||
:color="snackbar.color"
|
||||
:timeout="3000"
|
||||
location="top right"
|
||||
>
|
||||
{{ snackbar.message }}
|
||||
<template #actions>
|
||||
<v-btn icon="mdi-close" @click="snackbar.show = false" />
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { MenuItem, MenuForm } from '../../types/menu'
|
||||
import type { VForm } from 'vuetify/components'
|
||||
|
||||
definePageMeta({
|
||||
layout: 'default',
|
||||
title: 'Menu Builder'
|
||||
})
|
||||
|
||||
// Reactive data
|
||||
const isFormValid = ref(false)
|
||||
const isAdding = ref(false)
|
||||
const isLoading = ref(false)
|
||||
const isSaving = ref(false)
|
||||
const formRef = ref<VForm | null>(null)
|
||||
|
||||
const menuForm = ref<MenuForm>({
|
||||
titleMenu: '',
|
||||
sideMenu: '',
|
||||
nameMenu: '',
|
||||
linkUrlMenu: '',
|
||||
iconMenu: ''
|
||||
})
|
||||
|
||||
const snackbar = ref({
|
||||
show: false,
|
||||
message: '',
|
||||
color: 'success'
|
||||
})
|
||||
|
||||
// Form validation rules
|
||||
const rules = {
|
||||
required: (value: string) => !!value || 'This field is required'
|
||||
}
|
||||
|
||||
// Use composables
|
||||
const {
|
||||
menuItems,
|
||||
menuOptions,
|
||||
references,
|
||||
selectedReference,
|
||||
loadMenus,
|
||||
saveMenu,
|
||||
deleteMenu,
|
||||
updateMenuOrder
|
||||
} = useMenuManagement()
|
||||
|
||||
// Load initial data
|
||||
onMounted(async () => {
|
||||
await loadMenus()
|
||||
})
|
||||
|
||||
// Methods
|
||||
const addMenuItem = async () => {
|
||||
if (!isFormValid.value) return
|
||||
|
||||
isAdding.value = true
|
||||
try {
|
||||
const newItem: MenuItem = {
|
||||
id: `menu-${Date.now()}`,
|
||||
title: menuForm.value.titleMenu || menuForm.value.nameMenu,
|
||||
url: menuForm.value.linkUrlMenu,
|
||||
icon: menuForm.value.iconMenu || 'mdi-circle-outline',
|
||||
parentId: undefined,
|
||||
order: [...menuItems.value].length + 1,
|
||||
isActive: true,
|
||||
reference: selectedReference.value,
|
||||
children: [] as MenuItem[]
|
||||
}
|
||||
|
||||
await saveMenu({
|
||||
...newItem,
|
||||
children: [...(newItem.children ?? [])]
|
||||
})
|
||||
clearForm()
|
||||
showSnackbar('Menu item added successfully!', 'success')
|
||||
} catch (error) {
|
||||
showSnackbar('Failed to add menu item', 'error')
|
||||
} finally {
|
||||
isAdding.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const clearForm = () => {
|
||||
menuForm.value = {
|
||||
titleMenu: '',
|
||||
sideMenu: '',
|
||||
nameMenu: '',
|
||||
linkUrlMenu: '',
|
||||
iconMenu: ''
|
||||
}
|
||||
if (formRef.value) {
|
||||
formRef.value.resetValidation()
|
||||
}
|
||||
}
|
||||
|
||||
const editMenuItem = (item: MenuItem) => {
|
||||
menuForm.value = {
|
||||
titleMenu: item.title,
|
||||
sideMenu: '',
|
||||
nameMenu: item.title,
|
||||
linkUrlMenu: item.url ?? '',
|
||||
iconMenu: item.icon
|
||||
}
|
||||
}
|
||||
|
||||
const deleteMenuItem = async (itemId: string) => {
|
||||
try {
|
||||
await deleteMenu(itemId)
|
||||
showSnackbar('Menu item deleted successfully!', 'success')
|
||||
} catch (error) {
|
||||
showSnackbar('Failed to delete menu item', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const toggleMenuItem = async (itemId: string) => {
|
||||
const item = findMenuItem(itemId)
|
||||
if (item) {
|
||||
item.isActive = !item.isActive
|
||||
await saveMenu({
|
||||
...item,
|
||||
children: [...(item.children ?? [])]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const reloadMenu = async () => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
await loadMenus()
|
||||
showSnackbar('Menu reloaded successfully!', 'success')
|
||||
} catch (error) {
|
||||
showSnackbar('Failed to reload menu', 'error')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const saveAllMenus = async () => {
|
||||
isSaving.value = true
|
||||
try {
|
||||
// Save all menu changes
|
||||
for (const item of [...menuItems.value]) {
|
||||
// Cast children to mutable array if readonly
|
||||
const mutableItem = {
|
||||
...item,
|
||||
children: [...(item.children ?? [])]
|
||||
}
|
||||
await saveMenu(mutableItem)
|
||||
}
|
||||
showSnackbar('All menus saved successfully!', 'success')
|
||||
} catch (error) {
|
||||
showSnackbar('Failed to save menus', 'error')
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const findMenuItem = (id: string): MenuItem | null => {
|
||||
const findInItems = (items: MenuItem[], id: string): MenuItem | null => {
|
||||
for (const item of items) {
|
||||
if (item.id === id) return item
|
||||
if (item.children) {
|
||||
const found = findInItems(item.children, id)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
return findInItems(menuItems.value, id)
|
||||
}
|
||||
|
||||
const showSnackbar = (message: string, color: string) => {
|
||||
snackbar.value = { show: true, message, color }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.menu-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.menu-tree-container {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user