feat: add dashboard

This commit is contained in:
pa
2026-03-12 23:23:27 +09:00
parent 6e8f9543eb
commit e817d7392f
31 changed files with 2765 additions and 894 deletions

View File

@@ -30,6 +30,8 @@
:drag-state="dragState"
@edit-folder="openFolderEditor"
@delete-folder="handleDeleteFolder"
@edit-dashboard="openDashboardEditor"
@delete-dashboard="handleDeleteDashboard"
@hide="handleHideItem"
@toggle="handleTreeToggle(item)" />
</template>
@@ -73,6 +75,9 @@
<Button variant="outline" @click="handleAddFolder">
{{ t('nav_menu.custom_nav.new_folder') }}
</Button>
<Button variant="outline" @click="handleAddDashboard">
{{ t('dashboard.new_dashboard') }}
</Button>
<Button variant="ghost" class="text-destructive" @click="handleReset">
{{ t('nav_menu.custom_nav.restore_default') }}
</Button>
@@ -93,7 +98,13 @@
<Dialog v-model:open="folderEditor.visible">
<DialogContent class="sm:max-w-100">
<DialogHeader>
<DialogTitle>{{ t('nav_menu.custom_nav.edit_folder') }}</DialogTitle>
<DialogTitle>
{{
folderEditor.editorType === 'dashboard'
? t('nav_menu.custom_nav.edit_dashboard')
: t('nav_menu.custom_nav.edit_folder')
}}
</DialogTitle>
</DialogHeader>
<div class="flex flex-col gap-3">
<InputGroupField
@@ -159,6 +170,8 @@
import { Separator } from '../ui/separator';
import { Tree } from '../ui/tree';
import { navDefinitions } from '../../shared/constants/ui.js';
import { DASHBOARD_NAV_KEY_PREFIX, DEFAULT_DASHBOARD_ICON } from '../../shared/constants/dashboard';
import { useDashboardStore, useModalStore } from '../../stores';
import SortableTreeNode from './SortableTreeNode.vue';
@@ -178,11 +191,17 @@
defaultLayout: {
type: Array,
default: () => []
},
definitions: {
type: Array,
default: () => []
}
});
const emit = defineEmits(['update:visible', 'save']);
const emit = defineEmits(['update:visible', 'save', 'dashboard-created']);
const { t } = useI18n();
const dashboardStore = useDashboardStore();
const modalStore = useModalStore();
const cloneLayout = (source) => {
if (!Array.isArray(source)) return [];
@@ -210,6 +229,7 @@
visible: false,
isEditing: false,
editingId: null,
editorType: 'folder',
data: { id: '', name: '', icon: '' }
});
@@ -243,49 +263,53 @@
const definitionsMap = computed(() => {
const map = new Map();
navDefinitions.forEach((def) => {
const source = props.definitions?.length ? props.definitions : navDefinitions;
source.forEach((def) => {
if (def?.key) map.set(def.key, def);
});
return map;
});
const treeItems = computed(() => {
return localLayout.value.map((entry) => {
if (entry.type === 'folder') {
const children = (entry.items || [])
.map((key) => {
const def = definitionsMap.value.get(key);
if (!def) return null;
return { id: key, type: 'item', key, level: 1, parentId: entry.id };
})
.filter(Boolean);
return localLayout.value
.map((entry) => {
if (entry.type === 'folder') {
const children = (entry.items || [])
.map((key) => {
const def = definitionsMap.value.get(key);
if (!def) return null;
return { id: key, type: 'item', key, level: 1, parentId: entry.id };
})
.filter(Boolean);
const folderChildren = children.length
? children
: [{ id: `${entry.id}__placeholder`, _placeholder: true, level: 1 }];
const folderChildren = children.length
? children
: [{ id: `${entry.id}__placeholder`, _placeholder: true, level: 1 }];
return {
id: entry.id,
type: 'folder',
name: entry.name,
icon: entry.icon,
level: 0,
children: folderChildren
};
}
return { id: entry.key, type: 'item', key: entry.key, level: 0 };
});
return {
id: entry.id,
type: 'folder',
name: entry.name,
icon: entry.icon,
level: 0,
children: folderChildren
};
}
if (!definitionsMap.value.has(entry.key)) return null;
return { id: entry.key, type: 'item', key: entry.key, level: 0 };
})
.filter(Boolean);
});
const expandedKeys = ref([]);
const hiddenItems = computed(() =>
navDefinitions
(props.definitions?.length ? props.definitions : navDefinitions)
.filter((def) => hiddenKeySet.value.has(def.key))
.map((def) => ({
key: def.key,
icon: def.icon,
label: t(def.labelKey)
label: def.isDashboard ? def.labelKey : t(def.labelKey)
}))
);
@@ -646,13 +670,31 @@
if (!entry) return;
folderEditor.isEditing = true;
folderEditor.editingId = folderId;
folderEditor.editorType = 'folder';
folderEditor.data = { id: entry.id, name: entry.name, icon: entry.icon };
folderEditor.visible = true;
};
const openDashboardEditor = (dashboardKey) => {
const dashboardId = String(dashboardKey || '').replace(DASHBOARD_NAV_KEY_PREFIX, '');
const dashboard = dashboardStore.getDashboard(dashboardId);
if (!dashboard) return;
folderEditor.isEditing = true;
folderEditor.editingId = dashboardKey;
folderEditor.editorType = 'dashboard';
folderEditor.data = {
id: dashboardKey,
name: dashboard.name,
icon: dashboard.icon || ''
};
folderEditor.visible = true;
};
const handleAddFolder = () => {
folderEditor.isEditing = false;
folderEditor.editingId = null;
folderEditor.editorType = 'folder';
folderEditor.data = {
id: createFolderId(),
name: '',
@@ -661,10 +703,27 @@
folderEditor.visible = true;
};
const handleFolderEditorSave = () => {
const handleAddDashboard = async () => {
const dashboard = await dashboardStore.createDashboard(t('dashboard.default_name'));
dashboardStore.setEditingDashboardId(dashboard.id);
localLayout.value.push({
type: 'item',
key: `${DASHBOARD_NAV_KEY_PREFIX}${dashboard.id}`
});
localLayout.value = [...localLayout.value];
emit('dashboard-created', dashboard.id, cloneLayout(localLayout.value), [...hiddenKeySet.value]);
};
const handleFolderEditorSave = async () => {
if (!folderEditor.data.name?.trim()) return;
if (folderEditor.isEditing) {
if (folderEditor.editorType === 'dashboard') {
const dashboardId = String(folderEditor.editingId || '').replace(DASHBOARD_NAV_KEY_PREFIX, '');
await dashboardStore.updateDashboard(dashboardId, {
name: folderEditor.data.name.trim(),
icon: folderEditor.data.icon?.trim() || DEFAULT_DASHBOARD_ICON
});
} else if (folderEditor.isEditing) {
const entry = localLayout.value.find((e) => e.type === 'folder' && e.id === folderEditor.editingId);
if (entry) {
entry.name = folderEditor.data.name.trim();
@@ -689,6 +748,43 @@
folderEditor.visible = false;
};
const removeFromLayout = (key) => {
let removed = false;
for (let i = 0; i < localLayout.value.length; i++) {
const entry = localLayout.value[i];
if (entry.type === 'item' && entry.key === key) {
localLayout.value.splice(i, 1);
removed = true;
break;
}
if (entry.type === 'folder') {
const idx = entry.items?.indexOf(key);
if (idx !== undefined && idx >= 0) {
entry.items.splice(idx, 1);
removed = true;
}
}
}
if (removed) {
hiddenKeySet.value.delete(key);
hiddenPlacement.value.delete(key);
localLayout.value = [...localLayout.value];
}
};
const handleDeleteDashboard = async (dashboardKey) => {
const dashboardId = String(dashboardKey || '').replace(DASHBOARD_NAV_KEY_PREFIX, '');
const { ok } = await modalStore.confirm({
title: t('dashboard.confirmations.delete_title'),
description: t('dashboard.confirmations.delete_description')
});
if (!ok) {
return;
}
await dashboardStore.deleteDashboard(dashboardId);
removeFromLayout(dashboardKey);
};
const handleSave = () => {
const cleanedLayout = localLayout.value.filter(
(entry) => !(entry.type === 'folder' && (!entry.items || entry.items.length === 0))