@hostlink/nuxt-light 1.20.6 → 1.21.0

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.
@@ -1,16 +1,61 @@
1
1
  <script setup>
2
2
  import { useI18n } from "vue-i18n";
3
- import { ref, watch, computed } from 'vue';
3
+ import { ref, watch, computed, onMounted } from 'vue';
4
4
  import { useQuasar, format } from 'quasar';
5
5
  import { q, m, useLight, api } from '#imports';
6
6
  const { humanStorageSize } = format
7
7
 
8
8
  const light = useLight();
9
9
  const i18n = useI18n();
10
- const quasar = useQuasar();
11
- const $q = quasar;
10
+ const $q = useQuasar();
11
+
12
12
  const emit = defineEmits(["input", "close"]);
13
13
 
14
+ const { app: { drives } } = await q({
15
+ app: {
16
+ drives: {
17
+ index: true,
18
+ name: true,
19
+ }
20
+ }
21
+ });
22
+
23
+ const folders = ref([]);
24
+ const files = ref([]);
25
+
26
+
27
+ const nodes = drives.map((drive) => {
28
+ return {
29
+ icon: "sym_o_storage",
30
+ name: drive.name,
31
+ id: drive.index.toString(),
32
+ path: "/",
33
+ lazy: true,
34
+ driveIndex: drive.index,
35
+ type: "drive",
36
+ }
37
+ });
38
+
39
+
40
+ const onLazyLoad = async ({ node, key, done, fail }) => {
41
+
42
+ const folders = await api.drive(node.driveIndex).folders.list(node.path, {
43
+ name: true,
44
+ path: true
45
+ });
46
+
47
+ const data = folders.map((item) => {
48
+ item.driveIndex = node.driveIndex;
49
+ item.id = node.driveIndex + "/" + item.path;
50
+ item.lazy = true;
51
+ item.type = "folder";
52
+ return item;
53
+ });
54
+ done(data);
55
+ };
56
+
57
+
58
+
14
59
  const props = defineProps({
15
60
  closeable: Boolean,
16
61
  height: {
@@ -99,25 +144,15 @@ function changeDate(option) {
99
144
  function toggleLeftDrawer() {
100
145
  leftDrawerOpen.value = !leftDrawerOpen.value;
101
146
  }
147
+ const loadItems = async () => {
148
+ let path = selectedPath.value;
102
149
 
103
- const path = ref(props.base);
104
-
105
- const onLazyLoad = async ({ node, key, done, fail }) => {
106
- const data = await api.fs.folders.list(node.path);
107
- data.map((item) => {
108
- item.lazy = true;
109
- return item;
110
- });
111
- done(data);
112
- }
113
-
114
- const items = ref([]);
150
+ files.value = [];
151
+ folders.value = [];
115
152
 
116
- const loadItems = async () => {
117
153
  loading.value = true;
118
- items.value = [];
119
154
 
120
- let filesParams = { path: path.value };
155
+ let filesParams = { path };
121
156
 
122
157
  if (label.value) {
123
158
  filesParams.type = label.value;
@@ -127,42 +162,56 @@ const loadItems = async () => {
127
162
  filesParams.search = localSearch.value;
128
163
  }
129
164
 
130
- let files = await q("fsListFiles", filesParams, ["name", "size", "mime", "path", "canPreview", "imagePath", "lastModified", "lastModifiedHuman"]);
131
- files = files.map((item) => {
165
+ const resp = await q({
166
+ app: {
167
+ drive: {
168
+ __args: {
169
+ index: selectedDrive.value,
170
+ },
171
+ files: {
172
+ __args: filesParams,
173
+ name: true,
174
+ path: true,
175
+ size: true,
176
+ mime: true,
177
+ url: true,
178
+ lastModified: true,
179
+ lastModifiedHuman: true,
180
+ }
181
+ }
182
+ }
183
+ })
184
+
185
+ files.value = resp.app.drive.files.map((item) => {
186
+ item.driveIndex = selectedDrive.value;
132
187
  item.type = "file";
133
188
  return item;
134
189
  });
135
-
136
- let folders = [];
190
+ /* files.value = (await drive.value.files.list(path)).map((item) => {
191
+ item.driveIndex = selectedDrive.value;
192
+ item.type = "file";
193
+ return item;
194
+ });
195
+ */
137
196
  if (!label.value && !localSearch.value) {
138
- folders = await api.fs.folders.list(path.value);
139
- folders = folders.map((item) => {
197
+ folders.value = (await drive.value.folders.list(path)).map((item) => {
198
+ item.driveIndex = selectedDrive.value;
140
199
  item.type = "folder";
141
200
  item.lazy = true;
201
+ item.id = selectedDrive.value + "/" + item.path;
142
202
  return item;
143
203
  });
144
204
  }
145
- items.value = folders.concat(files);
146
205
 
147
206
  loading.value = false;
148
207
 
149
- return {
150
- files,
151
- folders,
152
- }
153
- }
154
208
 
155
- watch(path, async (val) => {
156
- selected.value = [];
157
- const items = await loadItems();
158
- reloadTreeFolder(path.value, items.folders);
159
209
 
160
- });
210
+ reloadTreeFolder(selectedNodePath.value, folders.value);
211
+ }
161
212
 
162
- const label = ref(null);
163
213
 
164
- const initItems = await loadItems();
165
- const folders = ref(initItems.folders);
214
+ const label = ref(null);
166
215
 
167
216
  watch(label, () => {
168
217
  loadItems();
@@ -171,18 +220,25 @@ watch(label, () => {
171
220
  const selected = ref([]);
172
221
 
173
222
  const breadcrumbs = computed(() => {
174
- let breadcrumbs = [
175
- {
176
- label: i18n.t("Storage"),
177
- path: props.base,
178
- },
179
- ];
180
-
181
- if (path.value.toString() == "") {
182
- return breadcrumbs;
223
+ if (selectedPath.value.toString() == "") {
224
+ return [];
183
225
  }
184
226
 
185
- let paths = path.value.split(props.base);
227
+ let paths = selectedNodePath.value.split("/");
228
+
229
+ //the first element is the drive index
230
+ let driveIndex = paths.shift();
231
+
232
+ //find the drive name
233
+ let driveName = drives.find((d) => d.index == driveIndex).name;
234
+
235
+
236
+ let breadcrumbs = [{
237
+ label: driveName,
238
+ path: driveIndex,
239
+ }]
240
+
241
+
186
242
 
187
243
  let ps = [];
188
244
  for (let p of paths) {
@@ -190,11 +246,12 @@ const breadcrumbs = computed(() => {
190
246
  ps.push(p);
191
247
  breadcrumbs.push({
192
248
  label: p,
193
- path: ps.join("/"),
249
+ path: driveIndex + "/" + ps.join("/"),
194
250
  });
195
251
  }
196
252
  }
197
253
 
254
+
198
255
  return breadcrumbs;
199
256
  });
200
257
 
@@ -211,7 +268,7 @@ const grid = ref(false);
211
268
 
212
269
  const onDblclickRow = (evt, row, index) => {
213
270
  if (row.type == "folder") {
214
- path.value = row.path;
271
+ selectedNodePath.value = row.driveIndex + "/" + row.path;
215
272
  return;
216
273
  }
217
274
  if (row.type == "file") {
@@ -234,79 +291,76 @@ const findFolder = (path, folders) => {
234
291
  }
235
292
 
236
293
  const reloadTreeFolder = (path, newFolders) => {
237
-
238
294
  let node = folderTree.value.getNodeByKey(path);
239
295
  if (node) {
240
296
  node.lazy = false;
241
297
  node.children = newFolders;
242
298
  folderTree.value.setExpanded(path, true);
243
- } else {
244
-
245
- folders.value = newFolders;
246
299
  }
247
300
 
248
301
  }
249
302
 
250
303
  const onDeleteSelected = () => {
251
- quasar.dialog({
304
+ $q.dialog({
252
305
  title: "Delete",
253
306
  message: "Are you sure you want to delete this files or folders?",
254
307
  cancel: true,
255
308
  }).onOk(async () => {
256
309
  for (let row of selected.value) {
257
310
  if (row.type == "folder") {
258
- await api.fs.folders.delete(row.path)
311
+ await drive.value.folders.delete(row.path)
259
312
  } else {
260
- await api.fs.files.delete(row.path)
313
+ await drive.value.files.delete(row.path)
261
314
  }
262
315
  }
263
316
  selected.value = [];
264
- const items = await loadItems();
265
- reloadTreeFolder(path.value, items.folders);
317
+ await loadItems();
318
+
266
319
  });
267
320
 
268
321
  }
269
322
 
270
323
  const onNewFolder = () => {
271
- quasar.dialog({
324
+ $q.dialog({
272
325
  title: "New Folder",
273
326
  prompt: {
327
+ model: "",
274
328
  label: "Name",
329
+ isValid: (val) => {
330
+ return val.length > 0;
331
+ }
275
332
  },
276
333
  cancel: true,
277
334
  }).onOk(async (name) => {
278
- await api.fs.folders.create(path.value + "/" + name);
279
- const items = await loadItems();
280
- reloadTreeFolder(path.value, items.folders);
335
+ await drive.value.folders.create(selectedPath.value + "/" + name);
336
+ await loadItems();
281
337
  });
282
338
  }
283
339
 
284
340
  const onDeleteRow = (row) => {
285
341
  if (row.type == "file") {
286
- quasar.dialog({
342
+ $q.dialog({
287
343
  title: "Delete",
288
344
  message: "Are you sure you want to delete this file?",
289
345
  cancel: true,
290
346
  }).onOk(async () => {
291
- await api.fs.files.delete(row.path);
292
- const items = await loadItems();
293
- reloadTreeFolder(path.value, items.folders);
347
+ await drive.value.files.delete(row.path);
348
+ await loadItems();
294
349
  });
295
350
  } else if (row.type == "folder") {
296
- quasar.dialog({
351
+ $q.dialog({
297
352
  title: "Delete",
298
353
  message: "Are you sure you want to delete this folder?",
299
354
  cancel: true,
300
355
  }).onOk(async () => {
301
- await api.fs.folders.delete(row.path);
302
- const items = await loadItems();
303
- reloadTreeFolder(path.value, items.folders);
356
+ await drive.value.folders.delete(row.path);
357
+ await loadItems();
304
358
  });
305
359
  }
306
360
  }
307
361
 
308
362
  const onRenameRow = (row) => {
309
- quasar.dialog({
363
+ $q.dialog({
310
364
  title: "Rename " + row.type,
311
365
  prompt: {
312
366
  label: "Name",
@@ -316,12 +370,12 @@ const onRenameRow = (row) => {
316
370
  }).onOk(async (name) => {
317
371
  try {
318
372
  if (row.type == "file") {
319
- await api.fs.files.rename(row.path, name)
373
+ await drive.value.files.rename(row.path, name)
320
374
  } else {
321
- await api.fs.folders.rename(row.path, name)
375
+ await drive.value.folders.rename(row.path, name)
322
376
  }
323
377
  } catch (e) {
324
- quasar.dialog({
378
+ $q.dialog({
325
379
  title: "Error",
326
380
  message: e.message,
327
381
  });
@@ -329,26 +383,23 @@ const onRenameRow = (row) => {
329
383
  return;
330
384
  }
331
385
 
332
- const items = await loadItems();
333
- reloadTreeFolder(path.value, items.folders);
386
+ await loadItems();
334
387
 
335
388
  });
336
389
  }
337
390
  const uploadFiles = ref([]);
338
391
  const showUploadFiles = ref(false);
339
392
 
340
-
341
393
  const onUploadFiles = async () => {
342
-
343
394
  $q.loading.show({
344
395
  message: "Uploading files...",
345
396
  });
346
397
 
347
398
  try {
348
399
  for (let file of uploadFiles.value) {
349
-
350
- await m("fsUploadFile", {
351
- path: path.value,
400
+ await m("lightDriveUploadFile", {
401
+ index: selectedDrive.value,
402
+ path: selectedPath.value,
352
403
  file
353
404
  });
354
405
  }
@@ -366,22 +417,32 @@ const onUploadFiles = async () => {
366
417
 
367
418
  showUploadFiles.value = false;
368
419
 
369
- const items = await loadItems();
370
- reloadTreeFolder(path.value, items.folders);
420
+ await loadItems();
371
421
 
372
422
  }
373
423
 
374
424
  const preview = ref(null);
375
425
 
426
+
427
+ const drive = computed(() => {
428
+ return api.drive(selectedDrive.value);
429
+ })
430
+
431
+
376
432
  const moveToFolder = async (folder) => {
377
433
 
378
434
  for (let row of selected.value) {
379
- m("fsMove", { path: row.path, target: folder })
435
+
436
+ if (row.type == "folder") {
437
+ await drive.value.folders.move(row.path, folder);
438
+ } else {
439
+ await drive.value.files.move(row.path, folder);
440
+ }
441
+
380
442
 
381
443
  }
382
444
 
383
- const items = await loadItems();
384
- reloadTreeFolder(path.value, items.folders);
445
+ await loadItems();
385
446
  }
386
447
 
387
448
  const submitSearch = (e) => {
@@ -389,27 +450,28 @@ const submitSearch = (e) => {
389
450
  loadItems();
390
451
  }
391
452
 
392
- /** for grid view */
393
- const foldersGrid = computed(() => {
394
- return items.value.filter((item) => {
395
- return item.type == "folder";
396
- });
397
- });
398
-
399
- const filesGrid = computed(() => {
400
- return items.value.filter((item) => {
401
- return item.type == "file";
402
- });
403
- });
404
-
405
453
  const onDownloadRow = async (row) => {
406
454
 
407
- const resp = await q("fsFile", {
408
- path: row.path
409
- }, ["base64Content"]);
455
+ const app = await q({
456
+ app: {
457
+ drive: {
458
+ __args: {
459
+ index: row.driveIndex,
460
+ },
461
+ file: {
462
+ __args: {
463
+ path: row.path,
464
+ },
465
+ base64Content: true,
466
+ }
467
+ }
468
+ }
469
+ })
470
+ console.log(app);
471
+ const base64Content = app.app.drive.file.base64Content;
410
472
 
411
473
  const downloadLink = document.createElement("a");
412
- downloadLink.href = `data:application/octet-stream;base64,${resp.base64Content}`;
474
+ downloadLink.href = `data:application/octet-stream;base64,${base64Content}`;
413
475
  downloadLink.download = row.name;
414
476
  downloadLink.click();
415
477
 
@@ -424,8 +486,8 @@ const reloadStorage = async () => {
424
486
  search.value = "";
425
487
  localSearch.value = "";
426
488
  label.value = null;
427
- const items = await loadItems();
428
- reloadTreeFolder(path.value, items.folders);
489
+ await loadItems();
490
+
429
491
  }
430
492
 
431
493
  const permission = await api.auth.granted([
@@ -467,17 +529,17 @@ const isDark = computed(() => light.isDarkMode());
467
529
 
468
530
  const onPreview = (row) => {
469
531
  showPreviewImgDialog.value = true;
470
- previewImg.value = row.imagePath;
532
+ previewImg.value = row.url;
471
533
  }
472
534
 
473
535
  const onPreviewPDF = async (row) => {
474
536
  const height = window.innerHeight - 200;
475
- quasar.dialog({
537
+ $q.dialog({
476
538
  autoClose: true,
477
539
  fullWidth: true,
478
540
  fullHeight: true,
479
541
  title: "Preview PDF",
480
- message: "<iframe src='" + row.imagePath + "' width='100%' height='" + height + "px'></iframe>",
542
+ message: "<iframe src='" + row.url + "' width='100%' height='" + height + "px'></iframe>",
481
543
  html: true,
482
544
 
483
545
  })
@@ -488,8 +550,84 @@ const onClickInfo = async (row) => {
488
550
 
489
551
  }
490
552
 
553
+ const items = computed(() => {
554
+ return [...folders.value, ...files.value];
555
+ })
556
+
557
+
558
+
559
+
560
+ const selectedNodePath = ref(null)
561
+ const selectedDrive = computed(() => {
562
+ if (selectedNodePath.value) {
563
+ //split the path into drive index and folder path
564
+ //examle 0/folder1/folder2
565
+ const [driveIndex, ...path] = selectedNodePath.value.split("/");
566
+ return parseInt(driveIndex);
567
+ }
568
+ return null;
569
+ })
570
+
571
+ const selectedPath = computed(() => {
572
+ if (selectedNodePath.value) {
573
+ //split the path into drive index and folder path
574
+ //examle 0/folder1/folder2
575
+ const [driveIndex, ...path] = selectedNodePath.value.split("/");
576
+ return "/" + path.join("/");
577
+ }
578
+ return "";
579
+ })
580
+
581
+ watch(selectedNodePath, async () => {
582
+ selected.value = [];
583
+ await loadItems()
584
+ });
585
+
586
+ const canPreview = (file) => {
587
+ //check file mime type
588
+ const mime = ['image/jpeg', 'image/png', 'image/gif'];
589
+ return mime.includes(file.mime);
590
+ };
591
+
592
+
593
+ const onCheckTotalSize = async (folder) => {
594
+
595
+ const d = $q.dialog({
596
+ title: "Total Size",
597
+ progress: true,
598
+
599
+ });
600
+
601
+ const resp = await q({
602
+ app: {
603
+ drive: {
604
+ __args: {
605
+ index: folder.driveIndex,
606
+ },
607
+ folder: {
608
+ __args: {
609
+ path: folder.path,
610
+ },
611
+ totalSize: true,
612
+ }
613
+ }
614
+
615
+ }
616
+ });
617
+
618
+ d.update({
619
+ message: humanStorageSize(resp.app.drive.folder.totalSize),
620
+ progress: false,
621
+ });
622
+
623
+
624
+ }
625
+
626
+
627
+ selectedNodePath.value = drives[0].index.toString();
491
628
  </script>
492
629
  <template>
630
+
493
631
  <q-layout view="hHh lpR fFf" :class="isDark ? '' : 'bg-white'" container :style="{ 'min-height': height }">
494
632
  <q-header bordered :class="isDark ? '' : 'bg-white text-grey-8'" height-hint="64">
495
633
  <q-toolbar>
@@ -518,7 +656,7 @@ const onClickInfo = async (row) => {
518
656
  <q-list padding class="text-grey-8">
519
657
  <q-item>
520
658
  <q-item-section>
521
- <q-btn icon="add" outline rounded :color="light.color" :label="$t('New')">
659
+ <q-btn icon="add" outline rounded :color="light.color" :label="$t('New')" :disable="!selectedNodePath">
522
660
  <q-menu>
523
661
  <q-list>
524
662
  <q-item clickable v-close-popup @click="onNewFolder" v-if="permission.includes('fs.folder.create')">
@@ -541,26 +679,10 @@ const onClickInfo = async (row) => {
541
679
  </q-item-section>
542
680
  </q-item>
543
681
 
544
- <q-list dense>
545
- <q-item>
546
- <q-item-section avatar>
547
- <q-icon name="sym_o_storage" />
548
- </q-item-section>
549
- <q-item-section>{{ $t('Storage') }}</q-item-section>
550
- <q-item-section avatar>
551
- <q-btn dense round flat icon="sym_o_refresh" @click="reloadStorage"></q-btn>
552
- </q-item-section>
553
- </q-item>
554
- <q-tree ref="folderTree" class="q-pl-md" :nodes="folders" node-key="path" label-key="name"
555
- @lazy-load="onLazyLoad" v-model:selected="path" no-selection-unset>
556
- </q-tree>
557
- </q-list>
558
- <!-- q-expansion-item :label="$t('Storage')" default-expand-all default-opened icon="sym_o_storage" selectable
559
- @click="path = '/'">
560
- <q-tree ref="folderTree" class="q-pl-md" :nodes="folders" node-key="path" label-key="name"
561
- @lazy-load="onLazyLoad" v-model:selected="path" no-selection-unset>
562
- </q-tree>
563
- </q-expansion-item-->
682
+
683
+ <q-tree ref="folderTree" :nodes="nodes" node-key="id" label-key="name" @lazy-load="onLazyLoad"
684
+ v-model:selected="selectedNodePath" no-selection-unset>
685
+ </q-tree>
564
686
 
565
687
  <q-separator inset class="q-my-sm" />
566
688
 
@@ -570,7 +692,7 @@ const onClickInfo = async (row) => {
570
692
  </q-drawer>
571
693
 
572
694
  <q-drawer v-model="rightDrawerOpen" side="right" show-if-above bordered>
573
- <l-file-manager-preview v-model="preview" v-if="preview" :key="preview.path" />
695
+ <l-file-manager-preview :path="preview.path" v-if="preview" :key="preview.path" :drive-index="selectedDrive" />
574
696
  </q-drawer>
575
697
 
576
698
  <q-page-container :style="{ height }">
@@ -598,14 +720,14 @@ const onClickInfo = async (row) => {
598
720
 
599
721
  <q-toolbar>
600
722
  <q-breadcrumbs :active-color="$light.color">
601
- <q-breadcrumbs-el v-for="(b, index) in breadcrumbs" :label="b.label" :key="index" @click="path = b.path"
602
- href="javascript:void(0)"></q-breadcrumbs-el>
723
+ <q-breadcrumbs-el v-for="(b, index) in breadcrumbs" :label="b.label" :key="index"
724
+ @click="selectedNodePath = b.path" href="javascript:void(0)"></q-breadcrumbs-el>
603
725
  </q-breadcrumbs>
604
726
  <q-space></q-space>
605
727
 
606
728
 
607
729
  <q-btn flat round icon="sym_o_drive_file_move" v-if="selected.length > 0">
608
- <l-file-manager-move @selected="moveToFolder($event)" />
730
+ <l-file-manager-move @selected="moveToFolder($event)" :drive-index="selectedDrive" />
609
731
  <q-tooltip>
610
732
  {{ $t('Move to') }}
611
733
  </q-tooltip>
@@ -624,7 +746,7 @@ const onClickInfo = async (row) => {
624
746
  </q-toolbar>
625
747
 
626
748
  <template v-if="grid">
627
- <q-table :title="$t('Folders')" flat grid :columns="columns" :rows="foldersGrid" hide-pagination
749
+ <q-table :title="$t('Folders')" flat grid :columns="columns" :rows="folders" hide-pagination
628
750
  :pagination="{ rowsPerPage: 0 }">
629
751
  <template v-slot:item="props">
630
752
  <div class="q-pa-xs col-xs-12 col-sm-6 col-md-4" @click="onDblclickRow(null, props.row, null)">
@@ -645,7 +767,7 @@ const onClickInfo = async (row) => {
645
767
  </template>
646
768
  </q-table>
647
769
 
648
- <q-table :title="$t('Files')" flat grid :columns="columns" :rows="filesGrid" hide-pagination
770
+ <q-table :title="$t('Files')" flat grid :columns="columns" :rows="files" hide-pagination
649
771
  :pagination="{ rowsPerPage: 0 }">
650
772
 
651
773
  <template v-slot:item="props">
@@ -655,15 +777,17 @@ const onClickInfo = async (row) => {
655
777
  <q-item-section avatar>
656
778
  <q-icon name="sym_o_description" size="sm"></q-icon>
657
779
  </q-item-section>
658
- <q-item-section no-wrap>
659
- {{ props.row.name }}
780
+ <q-item-section>
781
+ <q-item-label lines="1">
782
+ {{ props.row.name }}
783
+ </q-item-label>
660
784
  </q-item-section>
661
- <q-item-section avatar>
785
+ <q-item-section side>
662
786
  <q-checkbox v-model="selected" :val="props.row" :color="$light.color" />
663
787
  </q-item-section>
664
788
  </q-item>
665
789
 
666
- <q-img v-if="props.row.canPreview" :src="props.row.imagePath"></q-img>
790
+ <q-img v-if="canPreview(props.row)" :src="props.row.url"></q-img>
667
791
 
668
792
  </q-card>
669
793
  </div>
@@ -687,6 +811,15 @@ const onClickInfo = async (row) => {
687
811
  <q-btn flat icon="sym_o_more_vert" round dense>
688
812
  <q-menu>
689
813
  <q-list>
814
+
815
+ <q-item clickable v-close-popup="true" @click="onCheckTotalSize(props.row)"
816
+ v-if="props.row.type == 'folder'">
817
+ <q-item-section avatar>
818
+ <q-icon name="sym_o_info"></q-icon>
819
+ </q-item-section>
820
+ <q-item-section>{{ $t('Total size') }}</q-item-section>
821
+ </q-item>
822
+
690
823
  <q-item clickable v-close-popup @click="onDeleteRow(props.row)" v-if="canDeleteRow(props.row)">
691
824
  <q-item-section avatar>
692
825
  <q-icon name="sym_o_delete"></q-icon>
@@ -730,6 +863,8 @@ const onClickInfo = async (row) => {
730
863
  <q-item-section>{{ $t('Info') }}</q-item-section>
731
864
  </q-item>
732
865
 
866
+
867
+
733
868
  </q-list>
734
869
  </q-menu>
735
870
  </q-btn>