@coopenomics/desktop 2025.11.10-alpha-1 → 2025.11.10-alpha-2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -6
- package/src/app/layouts/default.vue +4 -0
- package/src/entities/CmdkMenu/index.ts +1 -0
- package/src/entities/CmdkMenu/model/index.ts +2 -0
- package/src/entities/CmdkMenu/model/store.ts +402 -0
- package/src/entities/CmdkMenu/model/types.ts +21 -0
- package/src/widgets/Desktop/CmdkMenu/CmdkMenu.vue +392 -0
- package/src/widgets/Desktop/CmdkMenu/index.ts +1 -0
- package/src/widgets/Desktop/CmdkTrigger/CmdkTrigger.vue +83 -0
- package/src/widgets/Desktop/CmdkTrigger/index.ts +1 -0
- package/src/widgets/Desktop/LeftDrawerMenu/LeftDrawerMenu.vue +2 -2
- package/src/widgets/Desktop/index.ts +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coopenomics/desktop",
|
|
3
|
-
"version": "2025.11.10-alpha-
|
|
3
|
+
"version": "2025.11.10-alpha-2",
|
|
4
4
|
"description": "A Desktop Project",
|
|
5
5
|
"productName": "Desktop App",
|
|
6
6
|
"author": "Alex Ant <dacom.dark.sun@gmail.com>",
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"start": "node -r ./alias-resolver.js dist/ssr/index.js"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@coopenomics/controller": "2025.11.10-alpha-
|
|
29
|
-
"@coopenomics/notifications": "2025.11.10-alpha-
|
|
30
|
-
"@coopenomics/sdk": "2025.11.10-alpha-
|
|
28
|
+
"@coopenomics/controller": "2025.11.10-alpha-2",
|
|
29
|
+
"@coopenomics/notifications": "2025.11.10-alpha-2",
|
|
30
|
+
"@coopenomics/sdk": "2025.11.10-alpha-2",
|
|
31
31
|
"@dicebear/collection": "^9.0.1",
|
|
32
32
|
"@dicebear/core": "^9.0.1",
|
|
33
33
|
"@editorjs/code": "^2.9.3",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"@wharfkit/wallet-plugin-privatekey": "^1.1.0",
|
|
60
60
|
"axios": "^1.2.1",
|
|
61
61
|
"compression": "^1.7.4",
|
|
62
|
-
"cooptypes": "2025.11.10-alpha-
|
|
62
|
+
"cooptypes": "2025.11.10-alpha-2",
|
|
63
63
|
"dompurify": "^3.1.7",
|
|
64
64
|
"dotenv": "^16.4.5",
|
|
65
65
|
"email-regex": "^5.0.0",
|
|
@@ -123,5 +123,5 @@
|
|
|
123
123
|
"npm": ">= 6.13.4",
|
|
124
124
|
"yarn": ">= 1.21.1"
|
|
125
125
|
},
|
|
126
|
-
"gitHead": "
|
|
126
|
+
"gitHead": "e914a117682b30c65a8fe762148bb579aa50d62d"
|
|
127
127
|
}
|
|
@@ -52,12 +52,16 @@ q-layout(view='lHh LpR fff')
|
|
|
52
52
|
WindowLoader(v-if='desktop?.isWorkspaceChanging')
|
|
53
53
|
router-view(v-else)
|
|
54
54
|
|
|
55
|
+
// Глобальный CmdkMenu на уровне всего приложения
|
|
56
|
+
CmdkMenu
|
|
57
|
+
|
|
55
58
|
</template>
|
|
56
59
|
|
|
57
60
|
<script setup lang="ts">
|
|
58
61
|
import { computed } from 'vue';
|
|
59
62
|
import { Header } from 'src/widgets/Header/CommonHeader';
|
|
60
63
|
import { LeftDrawerMenu } from 'src/widgets/Desktop/LeftDrawerMenu';
|
|
64
|
+
import { CmdkMenu } from 'src/widgets/Desktop/CmdkMenu';
|
|
61
65
|
import { ContactsFooter } from 'src/shared/ui/Footer';
|
|
62
66
|
|
|
63
67
|
import { useDesktopStore } from 'src/entities/Desktop/model';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './model';
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { defineStore } from 'pinia';
|
|
2
|
+
import { computed, ref, nextTick } from 'vue';
|
|
3
|
+
import { useRouter } from 'vue-router';
|
|
4
|
+
import { useCurrentUser } from 'src/entities/Session';
|
|
5
|
+
import { useDesktopStore } from 'src/entities/Desktop/model';
|
|
6
|
+
import { useSystemStore } from 'src/entities/System/model';
|
|
7
|
+
import type { PageItem, GroupedItem } from './types';
|
|
8
|
+
|
|
9
|
+
const namespace = 'cmdk-menu';
|
|
10
|
+
|
|
11
|
+
export const useCmdkMenuStore = defineStore(namespace, () => {
|
|
12
|
+
// Composables
|
|
13
|
+
const router = useRouter();
|
|
14
|
+
const user = useCurrentUser();
|
|
15
|
+
const desktop = useDesktopStore();
|
|
16
|
+
const { info } = useSystemStore();
|
|
17
|
+
|
|
18
|
+
// State
|
|
19
|
+
const showDialog = ref(false);
|
|
20
|
+
const searchQuery = ref('');
|
|
21
|
+
const selectedIndex = ref(0);
|
|
22
|
+
const selectedPageIndex = ref(-1);
|
|
23
|
+
const searchInput = ref<HTMLInputElement | null>(null);
|
|
24
|
+
const contentRef = ref<HTMLElement | null>(null);
|
|
25
|
+
|
|
26
|
+
// Функция для определения ОС и типа устройства
|
|
27
|
+
const getOSInfo = () => {
|
|
28
|
+
const userAgent = navigator.userAgent;
|
|
29
|
+
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
|
|
30
|
+
|
|
31
|
+
if (isMobile) {
|
|
32
|
+
return { os: 'mobile', shortcut: null };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Определение ОС для desktop
|
|
36
|
+
if (userAgent.includes('Mac')) {
|
|
37
|
+
return { os: 'mac', shortcut: '⌘ + K' };
|
|
38
|
+
} else if (userAgent.includes('Windows')) {
|
|
39
|
+
return { os: 'windows', shortcut: 'Ctrl + K' };
|
|
40
|
+
} else {
|
|
41
|
+
// Linux или другие Unix-like системы
|
|
42
|
+
return { os: 'linux', shortcut: 'Ctrl + K' };
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Вычисляемое свойство для подсказки горячих клавиш
|
|
47
|
+
const shortcutHint = computed(() => getOSInfo().shortcut);
|
|
48
|
+
|
|
49
|
+
// Функция для оценки условий
|
|
50
|
+
const evaluateCondition = (condition: string, context: Record<string, any>): boolean => {
|
|
51
|
+
try {
|
|
52
|
+
const func = new Function(...Object.keys(context), `return ${condition};`);
|
|
53
|
+
return func(...Object.values(context));
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error('Error evaluating condition:', error);
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Получение горячих клавиш для страниц
|
|
61
|
+
const getShortcut = (pageName: string): string | undefined => {
|
|
62
|
+
const shortcuts: Record<string, string> = {
|
|
63
|
+
'projects-list': '⌘P',
|
|
64
|
+
'capital-wallet': '⌘W',
|
|
65
|
+
'tracker': '⌘T',
|
|
66
|
+
'voting': '⌘V',
|
|
67
|
+
'results': '⌘R',
|
|
68
|
+
};
|
|
69
|
+
return shortcuts[pageName];
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Группировка воркспейсов с их страницами
|
|
73
|
+
const groupedItems = computed<GroupedItem[]>(() => {
|
|
74
|
+
const userRole = user.isChairman
|
|
75
|
+
? 'chairman'
|
|
76
|
+
: user.isMember
|
|
77
|
+
? 'member'
|
|
78
|
+
: 'user';
|
|
79
|
+
|
|
80
|
+
return desktop.workspaceMenus
|
|
81
|
+
.filter(
|
|
82
|
+
(item) =>
|
|
83
|
+
item.meta?.roles?.includes(userRole) ||
|
|
84
|
+
item.meta?.roles === undefined ||
|
|
85
|
+
item.meta?.roles.length === 0
|
|
86
|
+
)
|
|
87
|
+
.map(workspace => ({
|
|
88
|
+
workspaceName: workspace.workspaceName,
|
|
89
|
+
title: workspace.title,
|
|
90
|
+
icon: workspace.icon,
|
|
91
|
+
isActive: desktop.activeWorkspaceName === workspace.workspaceName,
|
|
92
|
+
pages: (workspace.mainRoute?.children || [])
|
|
93
|
+
.filter((page: any) => {
|
|
94
|
+
// Фильтрация по ролям, условиям и скрытым страницам
|
|
95
|
+
const rolesMatch =
|
|
96
|
+
page.meta?.roles?.includes(user.isChairman ? 'chairman' : user.isMember ? 'member' : 'user') ||
|
|
97
|
+
!page.meta?.roles ||
|
|
98
|
+
page.meta.roles.length === 0;
|
|
99
|
+
const conditionMatch = page.meta?.conditions
|
|
100
|
+
? evaluateCondition(page.meta.conditions, {
|
|
101
|
+
isCoop: user.privateAccount.value?.type === 'organization' &&
|
|
102
|
+
user.privateAccount.value?.organization_data?.type?.toUpperCase() === 'COOP',
|
|
103
|
+
userRole: user.isChairman ? 'chairman' : user.isMember ? 'member' : 'user',
|
|
104
|
+
userAccount: user.privateAccount.value,
|
|
105
|
+
coopname: info.coopname,
|
|
106
|
+
})
|
|
107
|
+
: true;
|
|
108
|
+
const hiddenMatch = page.meta?.hidden ? !page.meta.hidden : true;
|
|
109
|
+
|
|
110
|
+
return rolesMatch && conditionMatch && hiddenMatch;
|
|
111
|
+
})
|
|
112
|
+
.map((page: any) => ({
|
|
113
|
+
name: page.name,
|
|
114
|
+
meta: page.meta,
|
|
115
|
+
shortcut: getShortcut(page.name),
|
|
116
|
+
}))
|
|
117
|
+
}));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Поиск и фильтрация
|
|
121
|
+
const filteredItems = computed(() => {
|
|
122
|
+
if (!searchQuery.value) {
|
|
123
|
+
return groupedItems.value;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const query = searchQuery.value.toLowerCase();
|
|
127
|
+
|
|
128
|
+
return groupedItems.value
|
|
129
|
+
.map(group => ({
|
|
130
|
+
...group,
|
|
131
|
+
pages: group.pages.filter(page =>
|
|
132
|
+
page.meta.title.toLowerCase().includes(query) ||
|
|
133
|
+
page.name.toLowerCase().includes(query) ||
|
|
134
|
+
group.title.toLowerCase().includes(query) ||
|
|
135
|
+
group.workspaceName.toLowerCase().includes(query)
|
|
136
|
+
)
|
|
137
|
+
}))
|
|
138
|
+
.filter(group => group.pages.length > 0 || group.title.toLowerCase().includes(query));
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Методы
|
|
142
|
+
const openDialog = async () => {
|
|
143
|
+
showDialog.value = true;
|
|
144
|
+
await nextTick();
|
|
145
|
+
searchInput.value?.focus();
|
|
146
|
+
searchQuery.value = '';
|
|
147
|
+
selectedIndex.value = 0;
|
|
148
|
+
selectedPageIndex.value = -1;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const closeDialog = () => {
|
|
152
|
+
showDialog.value = false;
|
|
153
|
+
searchQuery.value = '';
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const handleSearch = () => {
|
|
157
|
+
selectedIndex.value = 0;
|
|
158
|
+
selectedPageIndex.value = -1;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const selectCurrentItem = () => {
|
|
162
|
+
if (filteredItems.value.length === 0) return;
|
|
163
|
+
|
|
164
|
+
const currentGroup = filteredItems.value[selectedIndex.value];
|
|
165
|
+
if (!currentGroup) return;
|
|
166
|
+
|
|
167
|
+
// Если выбрана страница (selectedPageIndex >= 0), переходим на неё
|
|
168
|
+
if (selectedPageIndex.value >= 0 && selectedPageIndex.value < currentGroup.pages.length) {
|
|
169
|
+
const selectedPage = currentGroup.pages[selectedPageIndex.value];
|
|
170
|
+
selectPage(currentGroup.workspaceName, selectedPage);
|
|
171
|
+
} else {
|
|
172
|
+
// Если выбрана группа (selectedPageIndex === -1), переходим на группу
|
|
173
|
+
selectGroup(selectedIndex.value);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const selectFirstItem = () => {
|
|
178
|
+
if (filteredItems.value.length > 0) {
|
|
179
|
+
const firstGroup = filteredItems.value[0];
|
|
180
|
+
if (firstGroup.pages.length > 0) {
|
|
181
|
+
selectPage(firstGroup.workspaceName, firstGroup.pages[0]);
|
|
182
|
+
} else {
|
|
183
|
+
selectGroup(0);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const scrollToSelected = () => {
|
|
189
|
+
if (!contentRef.value) return;
|
|
190
|
+
|
|
191
|
+
const selectedElement = contentRef.value.querySelector('.selected');
|
|
192
|
+
if (!selectedElement) return;
|
|
193
|
+
|
|
194
|
+
// Используем requestAnimationFrame для надежного ожидания обновления DOM
|
|
195
|
+
requestAnimationFrame(() => {
|
|
196
|
+
const container = contentRef.value;
|
|
197
|
+
if (!container) return;
|
|
198
|
+
|
|
199
|
+
// Простая логика: всегда прокручиваем выбранный элемент в верхнюю часть видимой области
|
|
200
|
+
selectedElement.scrollIntoView({
|
|
201
|
+
behavior: 'smooth',
|
|
202
|
+
block: 'start', // элемент будет по центру видимой области
|
|
203
|
+
inline: 'nearest'
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const navigateDown = async () => {
|
|
209
|
+
const totalGroups = filteredItems.value.length;
|
|
210
|
+
if (totalGroups === 0) return;
|
|
211
|
+
|
|
212
|
+
const currentGroup = filteredItems.value[selectedIndex.value];
|
|
213
|
+
const hasPages = currentGroup.pages.length > 0;
|
|
214
|
+
|
|
215
|
+
if (hasPages && selectedPageIndex.value < currentGroup.pages.length - 1) {
|
|
216
|
+
selectedPageIndex.value++;
|
|
217
|
+
} else if (selectedIndex.value < totalGroups - 1) {
|
|
218
|
+
selectedIndex.value++;
|
|
219
|
+
selectedPageIndex.value = -1;
|
|
220
|
+
} else {
|
|
221
|
+
selectedIndex.value = 0;
|
|
222
|
+
selectedPageIndex.value = -1;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Ждем обновления DOM перед скроллом
|
|
226
|
+
await nextTick();
|
|
227
|
+
scrollToSelected();
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const navigateUp = async () => {
|
|
231
|
+
const totalGroups = filteredItems.value.length;
|
|
232
|
+
if (totalGroups === 0) return;
|
|
233
|
+
|
|
234
|
+
if (selectedPageIndex.value > 0) {
|
|
235
|
+
selectedPageIndex.value--;
|
|
236
|
+
} else if (selectedIndex.value > 0) {
|
|
237
|
+
selectedIndex.value--;
|
|
238
|
+
const prevGroup = filteredItems.value[selectedIndex.value];
|
|
239
|
+
selectedPageIndex.value = prevGroup.pages.length > 0 ? prevGroup.pages.length - 1 : -1;
|
|
240
|
+
} else {
|
|
241
|
+
selectedIndex.value = totalGroups - 1;
|
|
242
|
+
const lastGroup = filteredItems.value[selectedIndex.value];
|
|
243
|
+
selectedPageIndex.value = lastGroup.pages.length > 0 ? lastGroup.pages.length - 1 : -1;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Ждем обновления DOM перед скроллом
|
|
247
|
+
await nextTick();
|
|
248
|
+
scrollToSelected();
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const selectGroup = (groupIndex: number) => {
|
|
252
|
+
const group = filteredItems.value[groupIndex];
|
|
253
|
+
if (!group) return;
|
|
254
|
+
|
|
255
|
+
closeDialog();
|
|
256
|
+
|
|
257
|
+
desktop.selectWorkspace(group.workspaceName);
|
|
258
|
+
setTimeout(() => {
|
|
259
|
+
desktop.goToDefaultPage(router);
|
|
260
|
+
}, 100);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const selectPage = (workspaceName: string, page: PageItem) => {
|
|
264
|
+
closeDialog();
|
|
265
|
+
|
|
266
|
+
// Если воркспейс не активен, переключаемся на него и ждем
|
|
267
|
+
if (desktop.activeWorkspaceName !== workspaceName) {
|
|
268
|
+
desktop.selectWorkspace(workspaceName);
|
|
269
|
+
// Ждем переключения воркспейса, затем переходим на страницу
|
|
270
|
+
setTimeout(() => {
|
|
271
|
+
router.push({
|
|
272
|
+
name: page.name,
|
|
273
|
+
params: { coopname: info.coopname },
|
|
274
|
+
});
|
|
275
|
+
// Сбрасываем состояние загрузки после перехода
|
|
276
|
+
setTimeout(() => {
|
|
277
|
+
desktop.setWorkspaceChanging(false);
|
|
278
|
+
}, 500);
|
|
279
|
+
}, 150);
|
|
280
|
+
} else {
|
|
281
|
+
// Воркспейс уже активен, переходим сразу
|
|
282
|
+
router.push({
|
|
283
|
+
name: page.name,
|
|
284
|
+
params: { coopname: info.coopname },
|
|
285
|
+
});
|
|
286
|
+
// Сбрасываем состояние загрузки после перехода
|
|
287
|
+
setTimeout(() => {
|
|
288
|
+
desktop.setWorkspaceChanging(false);
|
|
289
|
+
}, 500);
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// Обработчик клавиш для диалога (навигация внутри панели)
|
|
294
|
+
const handleDialogKeydown = (event: KeyboardEvent) => {
|
|
295
|
+
// Предотвращаем обработку глобального обработчика для диалога
|
|
296
|
+
event.stopPropagation();
|
|
297
|
+
|
|
298
|
+
switch (event.key) {
|
|
299
|
+
case 'ArrowDown':
|
|
300
|
+
event.preventDefault();
|
|
301
|
+
navigateDown();
|
|
302
|
+
break;
|
|
303
|
+
case 'ArrowUp':
|
|
304
|
+
event.preventDefault();
|
|
305
|
+
navigateUp();
|
|
306
|
+
break;
|
|
307
|
+
case 'Enter':
|
|
308
|
+
event.preventDefault();
|
|
309
|
+
selectCurrentItem();
|
|
310
|
+
break;
|
|
311
|
+
case 'Escape':
|
|
312
|
+
event.preventDefault();
|
|
313
|
+
closeDialog();
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// Глобальные горячие клавиши
|
|
319
|
+
const handleGlobalKeydown = (event: KeyboardEvent) => {
|
|
320
|
+
// Определяем правильную клавишу-модификатор для текущей ОС
|
|
321
|
+
const isMac = navigator.userAgent.includes('Mac');
|
|
322
|
+
const modifierPressed = isMac ? event.metaKey : event.ctrlKey;
|
|
323
|
+
|
|
324
|
+
// Проверяем комбинацию Ctrl/Cmd + K
|
|
325
|
+
if (modifierPressed && event.key.toLowerCase() === 'k') {
|
|
326
|
+
event.preventDefault();
|
|
327
|
+
event.stopPropagation();
|
|
328
|
+
|
|
329
|
+
if (showDialog.value) {
|
|
330
|
+
closeDialog();
|
|
331
|
+
} else {
|
|
332
|
+
openDialog();
|
|
333
|
+
}
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Escape для закрытия панели (только когда панель открыта)
|
|
338
|
+
if (event.key === 'Escape' && showDialog.value) {
|
|
339
|
+
event.preventDefault();
|
|
340
|
+
closeDialog();
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
// Инициализация глобальных обработчиков клавиш
|
|
345
|
+
const initGlobalKeydown = () => {
|
|
346
|
+
document.addEventListener('keydown', handleGlobalKeydown);
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const destroyGlobalKeydown = () => {
|
|
350
|
+
document.removeEventListener('keydown', handleGlobalKeydown);
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const setSearchInput = (input: HTMLInputElement | null) => {
|
|
354
|
+
searchInput.value = input as HTMLInputElement | null;
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const setContentRef = (ref: HTMLElement | null) => {
|
|
358
|
+
contentRef.value = ref;
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
// State
|
|
363
|
+
showDialog,
|
|
364
|
+
searchQuery,
|
|
365
|
+
selectedIndex,
|
|
366
|
+
selectedPageIndex,
|
|
367
|
+
searchInput,
|
|
368
|
+
contentRef,
|
|
369
|
+
|
|
370
|
+
// Computed
|
|
371
|
+
filteredItems,
|
|
372
|
+
shortcutHint,
|
|
373
|
+
activeWorkspaceIcon: computed(() => {
|
|
374
|
+
const activeWorkspace = desktop.workspaceMenus.find(
|
|
375
|
+
ws => ws.workspaceName === desktop.activeWorkspaceName
|
|
376
|
+
);
|
|
377
|
+
return activeWorkspace?.icon || 'fa-solid fa-desktop';
|
|
378
|
+
}),
|
|
379
|
+
activeWorkspaceTitle: computed(() => {
|
|
380
|
+
const activeWorkspace = desktop.workspaceMenus.find(
|
|
381
|
+
ws => ws.workspaceName === desktop.activeWorkspaceName
|
|
382
|
+
);
|
|
383
|
+
return activeWorkspace?.title || 'Рабочий стол';
|
|
384
|
+
}),
|
|
385
|
+
|
|
386
|
+
// Methods
|
|
387
|
+
openDialog,
|
|
388
|
+
closeDialog,
|
|
389
|
+
handleSearch,
|
|
390
|
+
handleDialogKeydown,
|
|
391
|
+
selectFirstItem,
|
|
392
|
+
selectCurrentItem,
|
|
393
|
+
navigateDown,
|
|
394
|
+
navigateUp,
|
|
395
|
+
selectGroup,
|
|
396
|
+
selectPage,
|
|
397
|
+
setSearchInput,
|
|
398
|
+
setContentRef,
|
|
399
|
+
initGlobalKeydown,
|
|
400
|
+
destroyGlobalKeydown,
|
|
401
|
+
};
|
|
402
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface PageItem {
|
|
2
|
+
name: string;
|
|
3
|
+
meta: any;
|
|
4
|
+
shortcut?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface GroupedItem {
|
|
8
|
+
workspaceName: string;
|
|
9
|
+
title: string;
|
|
10
|
+
icon: string;
|
|
11
|
+
isActive: boolean;
|
|
12
|
+
pages: PageItem[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CmdkMenuState {
|
|
16
|
+
showDialog: boolean;
|
|
17
|
+
searchQuery: string;
|
|
18
|
+
selectedIndex: number;
|
|
19
|
+
selectedPageIndex: number;
|
|
20
|
+
searchInput: HTMLInputElement | undefined;
|
|
21
|
+
}
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
<template lang="pug">
|
|
2
|
+
// Командная панель
|
|
3
|
+
q-dialog.cmdk-dialog(
|
|
4
|
+
v-model="cmdkStore.showDialog",
|
|
5
|
+
transition-show="slide-down",
|
|
6
|
+
transition-hide="slide-up",
|
|
7
|
+
@keydown="cmdkStore.handleDialogKeydown",
|
|
8
|
+
@click="cmdkStore.closeDialog"
|
|
9
|
+
)
|
|
10
|
+
.cmdk-panel
|
|
11
|
+
// Заголовок
|
|
12
|
+
.cmdk-header
|
|
13
|
+
q-icon.cmdk-search-icon(name="search")
|
|
14
|
+
input.cmdk-input(
|
|
15
|
+
autofocus,
|
|
16
|
+
ref="searchInput",
|
|
17
|
+
v-model="cmdkStore.searchQuery",
|
|
18
|
+
placeholder="Поиск рабочих столов и страниц...",
|
|
19
|
+
@input="cmdkStore.handleSearch"
|
|
20
|
+
)
|
|
21
|
+
q-btn.cmdk-close-btn(
|
|
22
|
+
flat,
|
|
23
|
+
dense,
|
|
24
|
+
round,
|
|
25
|
+
size="sm",
|
|
26
|
+
@click="cmdkStore.showDialog = false"
|
|
27
|
+
)
|
|
28
|
+
q-icon(name="close")
|
|
29
|
+
|
|
30
|
+
// Список результатов
|
|
31
|
+
div.cmdk-content(ref="contentRef")
|
|
32
|
+
transition-group.cmdk-results(
|
|
33
|
+
name="cmdk-fade",
|
|
34
|
+
tag="div"
|
|
35
|
+
)
|
|
36
|
+
// Пустое состояние
|
|
37
|
+
.cmdk-empty-state(
|
|
38
|
+
v-if="cmdkStore.filteredItems.length === 0 && cmdkStore.searchQuery",
|
|
39
|
+
key="empty"
|
|
40
|
+
)
|
|
41
|
+
q-icon(name="search_off", size="md")
|
|
42
|
+
.empty-text Ничего не найдено
|
|
43
|
+
.empty-subtext Попробуйте другой запрос
|
|
44
|
+
|
|
45
|
+
// Группы воркспейсов
|
|
46
|
+
template(v-else-if="cmdkStore.filteredItems.length > 0", v-for="(group, groupIndex) in cmdkStore.filteredItems", :key="group.workspaceName")
|
|
47
|
+
.cmdk-group
|
|
48
|
+
// Заголовок группы (воркспейс)
|
|
49
|
+
.cmdk-group-header(
|
|
50
|
+
:class="{ 'selected': cmdkStore.selectedIndex === groupIndex }"
|
|
51
|
+
@click="cmdkStore.selectGroup(groupIndex)"
|
|
52
|
+
)
|
|
53
|
+
q-icon.cmdk-group-icon(:name="group.icon")
|
|
54
|
+
.cmdk-group-title {{ group.title }}
|
|
55
|
+
.cmdk-group-badge(v-if="group.isActive") Активный
|
|
56
|
+
|
|
57
|
+
// Страницы внутри воркспейса
|
|
58
|
+
.cmdk-pages
|
|
59
|
+
.cmdk-page(
|
|
60
|
+
v-for="(page, pageIndex) in group.pages",
|
|
61
|
+
:key="page.name",
|
|
62
|
+
:class="{ 'selected': cmdkStore.selectedIndex === groupIndex && cmdkStore.selectedPageIndex === pageIndex }"
|
|
63
|
+
@click="cmdkStore.selectPage(group.workspaceName, page)"
|
|
64
|
+
)
|
|
65
|
+
q-icon.cmdk-page-icon(:name="page.meta.icon || 'circle'")
|
|
66
|
+
.cmdk-page-title {{ page.meta.title }}
|
|
67
|
+
.cmdk-page-shortcut(v-if="page.shortcut") {{ page.shortcut }}
|
|
68
|
+
|
|
69
|
+
// Подсказки
|
|
70
|
+
.cmdk-footer
|
|
71
|
+
.cmdk-hint
|
|
72
|
+
kbd ↑↓
|
|
73
|
+
span для навигации
|
|
74
|
+
kbd ↵ Enter
|
|
75
|
+
span для выбора
|
|
76
|
+
kbd ⎋ Esc
|
|
77
|
+
span для закрытия
|
|
78
|
+
</template>
|
|
79
|
+
|
|
80
|
+
<script setup lang="ts">
|
|
81
|
+
import { ref, onMounted, onUnmounted, watchEffect } from 'vue';
|
|
82
|
+
import { useCmdkMenuStore } from 'src/entities/CmdkMenu/model';
|
|
83
|
+
|
|
84
|
+
const cmdkStore = useCmdkMenuStore();
|
|
85
|
+
const searchInput = ref<HTMLInputElement | null>(null);
|
|
86
|
+
const contentRef = ref<HTMLElement | null>(null);
|
|
87
|
+
|
|
88
|
+
// Следим за contentRef и устанавливаем его в store
|
|
89
|
+
watchEffect(() => {
|
|
90
|
+
if (contentRef.value) {
|
|
91
|
+
cmdkStore.setContentRef(contentRef.value);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Инициализируем глобальные обработчики клавиш при монтировании компонента
|
|
96
|
+
onMounted(() => {
|
|
97
|
+
cmdkStore.initGlobalKeydown();
|
|
98
|
+
|
|
99
|
+
// Устанавливаем searchInput
|
|
100
|
+
if (searchInput.value) {
|
|
101
|
+
cmdkStore.setSearchInput(searchInput.value);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
onUnmounted(() => {
|
|
106
|
+
cmdkStore.destroyGlobalKeydown();
|
|
107
|
+
});
|
|
108
|
+
</script>
|
|
109
|
+
|
|
110
|
+
<style lang="scss">
|
|
111
|
+
.cmdk-dialog {
|
|
112
|
+
max-width: 100%;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.cmdk-panel {
|
|
116
|
+
width: 100%;
|
|
117
|
+
max-width: 600px;
|
|
118
|
+
background: white;
|
|
119
|
+
border-radius: 16px;
|
|
120
|
+
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
|
|
121
|
+
overflow: hidden;
|
|
122
|
+
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.cmdk-header {
|
|
126
|
+
display: flex;
|
|
127
|
+
align-items: center;
|
|
128
|
+
padding: 16px 20px;
|
|
129
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
|
130
|
+
background: #fafafa;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.cmdk-search-icon {
|
|
134
|
+
color: #666;
|
|
135
|
+
margin-right: 12px;
|
|
136
|
+
font-size: 18px;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.cmdk-input {
|
|
140
|
+
flex: 1;
|
|
141
|
+
border: none;
|
|
142
|
+
outline: none;
|
|
143
|
+
background: transparent;
|
|
144
|
+
font-size: 16px;
|
|
145
|
+
font-weight: 500;
|
|
146
|
+
color: #333;
|
|
147
|
+
|
|
148
|
+
&::placeholder {
|
|
149
|
+
color: #999;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.cmdk-close-btn {
|
|
154
|
+
margin-left: 12px;
|
|
155
|
+
opacity: 0.6;
|
|
156
|
+
|
|
157
|
+
&:hover {
|
|
158
|
+
opacity: 1;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.cmdk-content {
|
|
163
|
+
max-height: 400px;
|
|
164
|
+
overflow-y: auto;
|
|
165
|
+
|
|
166
|
+
&::-webkit-scrollbar {
|
|
167
|
+
width: 6px;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
&::-webkit-scrollbar-track {
|
|
171
|
+
background: rgba(0, 0, 0, 0.05);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
&::-webkit-scrollbar-thumb {
|
|
175
|
+
background: rgba(0, 105, 92, 0.3);
|
|
176
|
+
border-radius: 3px;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.cmdk-results {
|
|
181
|
+
padding: 8px 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.cmdk-group {
|
|
185
|
+
&:not(:last-child) {
|
|
186
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.cmdk-group-header {
|
|
191
|
+
display: flex;
|
|
192
|
+
align-items: center;
|
|
193
|
+
padding: 12px 20px;
|
|
194
|
+
cursor: pointer;
|
|
195
|
+
transition: all 0.2s ease;
|
|
196
|
+
user-select: none;
|
|
197
|
+
|
|
198
|
+
&:hover {
|
|
199
|
+
background: rgba(0, 105, 92, 0.04);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
&.selected {
|
|
203
|
+
background: rgba(0, 105, 92, 0.08);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.cmdk-group-icon {
|
|
208
|
+
margin-right: 12px;
|
|
209
|
+
color: #00695c;
|
|
210
|
+
font-size: 18px;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.cmdk-group-title {
|
|
214
|
+
flex: 1;
|
|
215
|
+
font-weight: 500;
|
|
216
|
+
color: #333;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.cmdk-group-badge {
|
|
220
|
+
background: #00695c;
|
|
221
|
+
color: white;
|
|
222
|
+
padding: 2px 8px;
|
|
223
|
+
border-radius: 12px;
|
|
224
|
+
font-size: 11px;
|
|
225
|
+
font-weight: 500;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.cmdk-pages {
|
|
229
|
+
background: rgba(0, 0, 0, 0.02);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.cmdk-page {
|
|
233
|
+
display: flex;
|
|
234
|
+
align-items: center;
|
|
235
|
+
padding: 10px 20px 10px 56px;
|
|
236
|
+
cursor: pointer;
|
|
237
|
+
transition: all 0.2s ease;
|
|
238
|
+
user-select: none;
|
|
239
|
+
|
|
240
|
+
&:hover {
|
|
241
|
+
background: rgba(0, 105, 92, 0.06);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
&.selected {
|
|
245
|
+
background: rgba(0, 105, 92, 0.1);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.cmdk-page-icon {
|
|
250
|
+
margin-right: 12px;
|
|
251
|
+
color: #666;
|
|
252
|
+
font-size: 16px;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.cmdk-page-title {
|
|
256
|
+
flex: 1;
|
|
257
|
+
font-size: 14px;
|
|
258
|
+
color: #555;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.cmdk-page-shortcut {
|
|
262
|
+
color: #999;
|
|
263
|
+
font-size: 12px;
|
|
264
|
+
font-weight: 500;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.cmdk-empty-state {
|
|
268
|
+
display: flex;
|
|
269
|
+
flex-direction: column;
|
|
270
|
+
align-items: center;
|
|
271
|
+
justify-content: center;
|
|
272
|
+
padding: 48px 20px;
|
|
273
|
+
color: #999;
|
|
274
|
+
|
|
275
|
+
.q-icon {
|
|
276
|
+
margin-bottom: 16px;
|
|
277
|
+
opacity: 0.5;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.empty-text {
|
|
281
|
+
font-size: 16px;
|
|
282
|
+
font-weight: 500;
|
|
283
|
+
margin-bottom: 4px;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.empty-subtext {
|
|
287
|
+
font-size: 14px;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.cmdk-footer {
|
|
292
|
+
padding: 12px 20px;
|
|
293
|
+
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
|
294
|
+
background: #fafafa;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.cmdk-hint {
|
|
298
|
+
display: flex;
|
|
299
|
+
align-items: center;
|
|
300
|
+
gap: 16px;
|
|
301
|
+
font-size: 12px;
|
|
302
|
+
color: #666;
|
|
303
|
+
|
|
304
|
+
kbd {
|
|
305
|
+
background: #e0e0e0;
|
|
306
|
+
border-radius: 4px;
|
|
307
|
+
padding: 2px 6px;
|
|
308
|
+
font-size: 11px;
|
|
309
|
+
font-weight: 500;
|
|
310
|
+
color: #333;
|
|
311
|
+
border: 1px solid #ccc;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
span {
|
|
315
|
+
margin-left: 4px;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Анимации
|
|
320
|
+
.cmdk-fade-enter-active,
|
|
321
|
+
.cmdk-fade-leave-active {
|
|
322
|
+
transition: all 0.2s ease;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.cmdk-fade-enter-from {
|
|
326
|
+
opacity: 0;
|
|
327
|
+
transform: translateY(-8px);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.cmdk-fade-leave-to {
|
|
331
|
+
opacity: 0;
|
|
332
|
+
transform: translateY(8px);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Темная тема
|
|
336
|
+
body.body--dark {
|
|
337
|
+
.cmdk-panel {
|
|
338
|
+
background: #1e1e1e;
|
|
339
|
+
border-color: rgba(255, 255, 255, 0.1);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.cmdk-header {
|
|
343
|
+
background: #2a2a2a;
|
|
344
|
+
border-color: rgba(255, 255, 255, 0.1);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.cmdk-input {
|
|
348
|
+
color: #e0e0e0;
|
|
349
|
+
|
|
350
|
+
&::placeholder {
|
|
351
|
+
color: #888;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.cmdk-group-header:hover {
|
|
356
|
+
background: rgba(255, 255, 255, 0.05);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.cmdk-group-header.selected {
|
|
360
|
+
background: rgba(0, 105, 92, 0.15);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.cmdk-group-title {
|
|
364
|
+
color: #e0e0e0;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.cmdk-page:hover {
|
|
368
|
+
background: rgba(255, 255, 255, 0.05);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.cmdk-page.selected {
|
|
372
|
+
background: rgba(0, 105, 92, 0.15);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.cmdk-page-title {
|
|
376
|
+
color: #ccc;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.cmdk-footer {
|
|
380
|
+
background: #2a2a2a;
|
|
381
|
+
border-color: rgba(255, 255, 255, 0.1);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.cmdk-hint {
|
|
385
|
+
color: #888;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.cmdk-empty-state {
|
|
389
|
+
color: #888;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {default as CmdkMenu} from './CmdkMenu.vue'
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<template lang="pug">
|
|
2
|
+
q-btn.cmdk-trigger(
|
|
3
|
+
flat,
|
|
4
|
+
dense,
|
|
5
|
+
stack,
|
|
6
|
+
@click="openCmdkDialog"
|
|
7
|
+
:class="{ 'active': cmdkStore.activeWorkspaceIcon !== 'fa-solid fa-desktop' }"
|
|
8
|
+
)
|
|
9
|
+
.trigger-content
|
|
10
|
+
q-icon.trigger-icon(:name="cmdkStore.activeWorkspaceIcon")
|
|
11
|
+
.trigger-text {{ cmdkStore.activeWorkspaceTitle }}
|
|
12
|
+
.cmdk-shortcut(v-if="cmdkStore.shortcutHint") {{ cmdkStore.shortcutHint }}
|
|
13
|
+
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup lang="ts">
|
|
17
|
+
import { useCmdkMenuStore } from 'src/entities/CmdkMenu/model';
|
|
18
|
+
|
|
19
|
+
const cmdkStore = useCmdkMenuStore();
|
|
20
|
+
|
|
21
|
+
const openCmdkDialog = () => {
|
|
22
|
+
cmdkStore.openDialog();
|
|
23
|
+
};
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<style lang="scss">
|
|
27
|
+
.cmdk-trigger {
|
|
28
|
+
width: 100%;
|
|
29
|
+
height: 100px;
|
|
30
|
+
border-radius: 0 !important;
|
|
31
|
+
background: transparent;
|
|
32
|
+
border: none;
|
|
33
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
34
|
+
position: relative;
|
|
35
|
+
align-items: center;
|
|
36
|
+
justify-content: center;
|
|
37
|
+
|
|
38
|
+
&:hover {
|
|
39
|
+
background: rgba(0, 105, 92, 0.04);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
&.active {
|
|
43
|
+
background: rgba(0, 105, 92, 0.04);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.trigger-content {
|
|
47
|
+
display: flex;
|
|
48
|
+
flex-direction: column;
|
|
49
|
+
align-items: center;
|
|
50
|
+
gap: 4px;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.trigger-icon {
|
|
54
|
+
font-size: 20px;
|
|
55
|
+
color: #00695c;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.trigger-text {
|
|
59
|
+
font-size: 10px;
|
|
60
|
+
font-weight: 500;
|
|
61
|
+
color: #424242;
|
|
62
|
+
text-align: center;
|
|
63
|
+
max-width: 100%;
|
|
64
|
+
overflow: hidden;
|
|
65
|
+
text-overflow: ellipsis;
|
|
66
|
+
white-space: nowrap;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.cmdk-shortcut {
|
|
70
|
+
position: absolute;
|
|
71
|
+
top: 4px;
|
|
72
|
+
right: 4px;
|
|
73
|
+
font-size: 8px;
|
|
74
|
+
color: #999;
|
|
75
|
+
font-weight: 500;
|
|
76
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
77
|
+
line-height: 1;
|
|
78
|
+
padding: 1px 3px;
|
|
79
|
+
border-radius: 3px;
|
|
80
|
+
pointer-events: none;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as CmdkTrigger } from './CmdkTrigger.vue';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template lang="pug">
|
|
2
2
|
.left-drawer-menu
|
|
3
3
|
.menu-content
|
|
4
|
-
|
|
4
|
+
CmdkTrigger
|
|
5
5
|
SecondLevelMenuList
|
|
6
6
|
.bottom-section
|
|
7
7
|
.toggle-button-wrapper
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
<script setup lang="ts">
|
|
28
28
|
import { ref, onMounted } from 'vue';
|
|
29
29
|
import { SecondLevelMenuList } from '../SecondLevelMenuList';
|
|
30
|
-
import {
|
|
30
|
+
import { CmdkTrigger } from '../CmdkTrigger';
|
|
31
31
|
import { LogoutButton } from 'src/features/User/Logout';
|
|
32
32
|
import { MicroWallet } from 'src/widgets/Wallet';
|
|
33
33
|
|