@hostlink/nuxt-light 1.50.0 → 1.51.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.
- package/dist/module.json +1 -1
- package/dist/module.mjs +13 -3
- package/dist/runtime/components/l-app-main.d.vue.ts +3 -3
- package/dist/runtime/components/l-app-main.vue +14 -4
- package/dist/runtime/components/l-app-main.vue.d.ts +3 -3
- package/dist/runtime/components/l-table.vue +1 -12
- package/dist/runtime/pages/System/database/check.vue +149 -0
- package/dist/runtime/pages/System/database/table.vue +2 -3
- package/dist/runtime/pages/Translate/index.vue +5 -5
- package/dist/runtime/pages/User/setting/favorite.d.vue.ts +2 -0
- package/dist/runtime/pages/User/setting/favorite.vue +164 -0
- package/dist/runtime/pages/User/setting/favorite.vue.d.ts +2 -0
- package/dist/runtime/pages/User/setting/index.vue +0 -1
- package/dist/runtime/pages/User/setting/menu.d.vue.ts +2 -0
- package/dist/runtime/pages/User/setting/menu.vue +422 -0
- package/dist/runtime/pages/User/setting/menu.vue.d.ts +2 -0
- package/dist/runtime/pages/User/setting.vue +4 -2
- package/package.json +1 -1
- package/dist/runtime/pages/User/setting/my_favorite.vue +0 -156
- /package/dist/runtime/pages/{User/setting/my_favorite.d.vue.ts → System/database/check.d.vue.ts} +0 -0
- /package/dist/runtime/pages/{User/setting/my_favorite.vue.d.ts → System/database/check.vue.d.ts} +0 -0
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -131,6 +131,11 @@ const routes = [
|
|
|
131
131
|
path: "/SystemValue/add",
|
|
132
132
|
file: "runtime/pages/SystemValue/add.vue"
|
|
133
133
|
},
|
|
134
|
+
{
|
|
135
|
+
name: "System-database-check",
|
|
136
|
+
path: "/System/database/check",
|
|
137
|
+
file: "runtime/pages/System/database/check.vue"
|
|
138
|
+
},
|
|
134
139
|
{
|
|
135
140
|
name: "System-database-process",
|
|
136
141
|
path: "/System/database/process",
|
|
@@ -226,9 +231,14 @@ const routes = [
|
|
|
226
231
|
file: "runtime/pages/User/setting/information.vue"
|
|
227
232
|
},
|
|
228
233
|
{
|
|
229
|
-
name: "User-setting-
|
|
230
|
-
path: "
|
|
231
|
-
file: "runtime/pages/User/setting/
|
|
234
|
+
name: "User-setting-favorite",
|
|
235
|
+
path: "favorite",
|
|
236
|
+
file: "runtime/pages/User/setting/favorite.vue"
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: "User-setting-menu",
|
|
240
|
+
path: "menu",
|
|
241
|
+
file: "runtime/pages/User/setting/menu.vue"
|
|
232
242
|
},
|
|
233
243
|
{
|
|
234
244
|
name: "User-setting-open_id",
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
declare var __VLS_30: {}, __VLS_180: {},
|
|
1
|
+
declare var __VLS_30: {}, __VLS_180: {}, __VLS_271: {}, __VLS_277: {};
|
|
2
2
|
type __VLS_Slots = {} & {
|
|
3
3
|
header?: (props: typeof __VLS_30) => any;
|
|
4
4
|
} & {
|
|
5
5
|
'user-menu'?: (props: typeof __VLS_180) => any;
|
|
6
6
|
} & {
|
|
7
|
-
'page-top'?: (props: typeof
|
|
7
|
+
'page-top'?: (props: typeof __VLS_271) => any;
|
|
8
8
|
} & {
|
|
9
|
-
'page-bottom'?: (props: typeof
|
|
9
|
+
'page-bottom'?: (props: typeof __VLS_277) => any;
|
|
10
10
|
};
|
|
11
11
|
declare const __VLS_component: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
12
12
|
logout: (...args: any[]) => void;
|
|
@@ -3,7 +3,7 @@ import { useRoute, useRouter } from "vue-router";
|
|
|
3
3
|
import { useLight, q, m } from "#imports";
|
|
4
4
|
import { useQuasar } from "quasar";
|
|
5
5
|
import { useI18n } from "vue-i18n";
|
|
6
|
-
import { ref, computed, reactive, provide, watch, toRaw
|
|
6
|
+
import { ref, computed, reactive, provide, watch, toRaw } from "vue";
|
|
7
7
|
import { useRuntimeConfig } from "nuxt/app";
|
|
8
8
|
import { api } from "#imports";
|
|
9
9
|
const emits = defineEmits(["logout"]);
|
|
@@ -47,7 +47,8 @@ const tt = await q({
|
|
|
47
47
|
label: true,
|
|
48
48
|
path: true,
|
|
49
49
|
icon: true
|
|
50
|
-
}
|
|
50
|
+
},
|
|
51
|
+
menu: true
|
|
51
52
|
}
|
|
52
53
|
});
|
|
53
54
|
let app = tt.app;
|
|
@@ -58,7 +59,6 @@ light.init(my.styles);
|
|
|
58
59
|
light.setMyRoles(my.roles);
|
|
59
60
|
light.setPermissions(my.permissions);
|
|
60
61
|
light.setMyFavorites(toRaw(my.myFavorites));
|
|
61
|
-
const _errorTimers = /* @__PURE__ */ new Map();
|
|
62
62
|
const myFavorites = computed(() => {
|
|
63
63
|
return light.getMyFavorites();
|
|
64
64
|
});
|
|
@@ -75,6 +75,7 @@ for (let t of app.i18nMessages) {
|
|
|
75
75
|
}
|
|
76
76
|
i18n.setLocaleMessage(i18n.locale, messages);
|
|
77
77
|
const menus = ref(app.menus);
|
|
78
|
+
const my_menus = ref(my.menu);
|
|
78
79
|
const leftDrawerOpen = ref(false);
|
|
79
80
|
const rightDrawerOpen = ref(false);
|
|
80
81
|
const toggleLeftDrawer = () => {
|
|
@@ -126,8 +127,16 @@ const onChangeLocale = async (locale) => {
|
|
|
126
127
|
window.location.reload();
|
|
127
128
|
};
|
|
128
129
|
const reloadMenu = async () => {
|
|
129
|
-
let app2 = await q(
|
|
130
|
+
let { app: app2, my: my2 } = await q({
|
|
131
|
+
app: {
|
|
132
|
+
menus: true
|
|
133
|
+
},
|
|
134
|
+
my: {
|
|
135
|
+
menu: true
|
|
136
|
+
}
|
|
137
|
+
});
|
|
130
138
|
menus.value = app2.menus;
|
|
139
|
+
my_menus.value.menu = my2.menu;
|
|
131
140
|
};
|
|
132
141
|
provide("reloadMenu", reloadMenu);
|
|
133
142
|
watch(() => style.footer, async (value) => await light.setStyle("footer", value));
|
|
@@ -346,6 +355,7 @@ const onLogout = async () => {
|
|
|
346
355
|
<!-- drawer content -->
|
|
347
356
|
<q-scroll-area class="fit">
|
|
348
357
|
<div class="q-mx-xs q-mt-xs">
|
|
358
|
+
<l-menu :value="my_menus" :dense="style.dense" />
|
|
349
359
|
<l-fav-menu :value="myFavorites" :dense="style.dense" v-if="myFavoritesCount > 0" />
|
|
350
360
|
<l-menu :value="menus" :dense="style.dense" />
|
|
351
361
|
</div>
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
declare var __VLS_30: {}, __VLS_180: {},
|
|
1
|
+
declare var __VLS_30: {}, __VLS_180: {}, __VLS_271: {}, __VLS_277: {};
|
|
2
2
|
type __VLS_Slots = {} & {
|
|
3
3
|
header?: (props: typeof __VLS_30) => any;
|
|
4
4
|
} & {
|
|
5
5
|
'user-menu'?: (props: typeof __VLS_180) => any;
|
|
6
6
|
} & {
|
|
7
|
-
'page-top'?: (props: typeof
|
|
7
|
+
'page-top'?: (props: typeof __VLS_271) => any;
|
|
8
8
|
} & {
|
|
9
|
-
'page-bottom'?: (props: typeof
|
|
9
|
+
'page-bottom'?: (props: typeof __VLS_277) => any;
|
|
10
10
|
};
|
|
11
11
|
declare const __VLS_component: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
12
12
|
logout: (...args: any[]) => void;
|
|
@@ -431,17 +431,6 @@ const getCellStyle = (col, row) => {
|
|
|
431
431
|
}
|
|
432
432
|
return style;
|
|
433
433
|
};
|
|
434
|
-
const getCellClass = (col, row) => {
|
|
435
|
-
const cl = [];
|
|
436
|
-
if (col.cellClass) {
|
|
437
|
-
if (typeof col.cellClass == "function") {
|
|
438
|
-
cl.push(col.cellClass(row));
|
|
439
|
-
} else {
|
|
440
|
-
cl.push(col.cellClass);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
return cl;
|
|
444
|
-
};
|
|
445
434
|
const localSelected = computed({
|
|
446
435
|
get() {
|
|
447
436
|
return props.selected;
|
|
@@ -574,7 +563,7 @@ const hasFilters = computed(() => {
|
|
|
574
563
|
</template>
|
|
575
564
|
<template v-else>
|
|
576
565
|
<q-td :key="col.name" :props="props" :auto-width="col.autoWidth ?? false"
|
|
577
|
-
:style="getCellStyle(col, props.row)"
|
|
566
|
+
:style="getCellStyle(col, props.row)"><template
|
|
578
567
|
v-if="col.to" class="bg-primary">
|
|
579
568
|
<l-link :to="col.to(props.row)" v-if="col.to(props.row)">{{ col.value }}</l-link>
|
|
580
569
|
</template>
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref } from "vue";
|
|
3
|
+
import { q, m, useAsyncData, useQuasar } from "#imports";
|
|
4
|
+
const $q = useQuasar();
|
|
5
|
+
const { data: system, refresh } = await useAsyncData("database-check", async () => {
|
|
6
|
+
const result = await q({
|
|
7
|
+
system: {
|
|
8
|
+
database: {
|
|
9
|
+
checkResult: true
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
return result.system;
|
|
14
|
+
});
|
|
15
|
+
const expanded = ref([]);
|
|
16
|
+
const tableColumns = [
|
|
17
|
+
{ name: "table", label: "Table", field: "table", align: "left" },
|
|
18
|
+
{ name: "status", label: "Status", field: "status", align: "center" },
|
|
19
|
+
{ name: "count", label: "Differences", field: (row) => row.differences.length, align: "center" },
|
|
20
|
+
{ name: "action", label: "Action", field: "action", align: "center" }
|
|
21
|
+
];
|
|
22
|
+
const diffColumns = [
|
|
23
|
+
{ name: "type", label: "Type", field: "type", align: "left" },
|
|
24
|
+
{ name: "column", label: "Column", field: "column", align: "left" },
|
|
25
|
+
{ name: "details", label: "Details", field: "details", align: "left" }
|
|
26
|
+
];
|
|
27
|
+
const getStatusColor = (status) => status === "OK" ? "positive" : "warning";
|
|
28
|
+
const getDiffDetails = (diff) => {
|
|
29
|
+
if (diff.type === "missing_column") {
|
|
30
|
+
return `Missing: ${diff.expected.name} (${diff.expected.type})`;
|
|
31
|
+
}
|
|
32
|
+
if (diff.type === "extra_column") {
|
|
33
|
+
return `Extra: ${diff.current.type}${diff.current.length ? `(${diff.current.length})` : ""}`;
|
|
34
|
+
}
|
|
35
|
+
if (diff.type === "column_mismatch") {
|
|
36
|
+
return Object.entries(diff.differences).map(([key, val]) => `${key}: ${JSON.stringify(val)}`).join(" | ");
|
|
37
|
+
}
|
|
38
|
+
if (diff.current) {
|
|
39
|
+
return JSON.stringify(diff.current).substring(0, 100) + "...";
|
|
40
|
+
}
|
|
41
|
+
return "";
|
|
42
|
+
};
|
|
43
|
+
const getDiffTypeColor = (type) => {
|
|
44
|
+
switch (type) {
|
|
45
|
+
case "missing_column":
|
|
46
|
+
return "negative";
|
|
47
|
+
case "extra_column":
|
|
48
|
+
return "info";
|
|
49
|
+
case "column_mismatch":
|
|
50
|
+
return "warning";
|
|
51
|
+
default:
|
|
52
|
+
return "grey";
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const toggleExpanded = (tableName) => {
|
|
56
|
+
const index = expanded.value.indexOf(tableName);
|
|
57
|
+
if (index === -1) {
|
|
58
|
+
expanded.value.push(tableName);
|
|
59
|
+
} else {
|
|
60
|
+
expanded.value.splice(index, 1);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const handleFix = async (tableName) => {
|
|
64
|
+
$q.dialog({
|
|
65
|
+
title: "Confirm Fix",
|
|
66
|
+
message: `Are you sure you want to fix the table "${tableName}"? This action cannot be undone.`,
|
|
67
|
+
cancel: true,
|
|
68
|
+
persistent: true
|
|
69
|
+
}).onOk(async () => {
|
|
70
|
+
await m("fixDatabaseTable", { name: tableName });
|
|
71
|
+
await refresh();
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
</script>
|
|
75
|
+
|
|
76
|
+
<template>
|
|
77
|
+
<l-page>
|
|
78
|
+
|
|
79
|
+
<div class="q-mb-md">
|
|
80
|
+
<h5 class="q-ma-none">Database Check Results</h5>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<q-table :rows="system.database.checkResult" :columns="tableColumns" row-key="table" flat bordered
|
|
84
|
+
class="q-mb-lg" v-model:expanded="expanded" :pagination.sync="{ rowsPerPage: 0 }" hide-bottom>
|
|
85
|
+
<template #body="props">
|
|
86
|
+
<q-tr :props="props"
|
|
87
|
+
:class="{ 'bg-red-1': props.row.status === 'DIFFERENT', 'bg-green-1': props.row.status === 'OK' }">
|
|
88
|
+
<q-td auto-width>
|
|
89
|
+
<q-btn size="sm" flat dense round
|
|
90
|
+
:icon="expanded.includes(props.row.table) ? 'expand_less' : 'expand_more'"
|
|
91
|
+
@click="toggleExpanded(props.row.table)" />
|
|
92
|
+
</q-td>
|
|
93
|
+
<q-td key="table" :props="props" class="text-weight-bold">
|
|
94
|
+
{{ props.row.table }}
|
|
95
|
+
</q-td>
|
|
96
|
+
<q-td key="status" :props="props">
|
|
97
|
+
<q-chip :label="props.row.status" :color="getStatusColor(props.row.status)" text-color="white"
|
|
98
|
+
size="sm" />
|
|
99
|
+
</q-td>
|
|
100
|
+
<q-td key="count" :props="props">
|
|
101
|
+
<q-chip v-if="props.row.differences.length > 0" :label="props.row.differences.length"
|
|
102
|
+
color="warning" text-color="white" size="sm" />
|
|
103
|
+
<span v-else class="text-positive">✓</span>
|
|
104
|
+
</q-td>
|
|
105
|
+
<q-td key="action" :props="props">
|
|
106
|
+
<q-btn v-if="props.row.status !== 'OK'" label="Fix" size="sm" :color="$light.color"
|
|
107
|
+
@click="handleFix(props.row.table)" />
|
|
108
|
+
</q-td>
|
|
109
|
+
</q-tr>
|
|
110
|
+
|
|
111
|
+
<!-- Differences Details Expansion -->
|
|
112
|
+
<q-tr v-show="expanded.includes(props.row.table)" :props="props">
|
|
113
|
+
<q-td colspan="100%" class="q-pa-none">
|
|
114
|
+
<div class="q-pa-md bg-grey-1">
|
|
115
|
+
<q-table v-if="props.row.differences.length > 0" :rows="props.row.differences"
|
|
116
|
+
:columns="diffColumns" row-key="column" flat dense class="q-mb-md"
|
|
117
|
+
:pagination.sync="{ rowsPerPage: 0 }" hide-bottom>
|
|
118
|
+
<template #body-cell-type="cellProps">
|
|
119
|
+
<q-td :props="cellProps">
|
|
120
|
+
<q-chip :label="cellProps.row.type"
|
|
121
|
+
:color="getDiffTypeColor(cellProps.row.type)" text-color="white"
|
|
122
|
+
size="sm" />
|
|
123
|
+
</q-td>
|
|
124
|
+
</template>
|
|
125
|
+
<template #body-cell-details="cellProps">
|
|
126
|
+
<q-td :props="cellProps">
|
|
127
|
+
<q-expansion-item header-class="text-caption" expand-icon-class="text-caption"
|
|
128
|
+
dense>
|
|
129
|
+
<template #header>
|
|
130
|
+
<span class="text-caption">{{ getDiffDetails(cellProps.row) }}</span>
|
|
131
|
+
</template>
|
|
132
|
+
<pre class="q-ma-none text-caption">{{
|
|
133
|
+
JSON.stringify(cellProps.row.differences || cellProps.row.current ||
|
|
134
|
+
cellProps.row.expected, null,
|
|
135
|
+
2) }}
|
|
136
|
+
</pre>
|
|
137
|
+
</q-expansion-item>
|
|
138
|
+
</q-td>
|
|
139
|
+
</template>
|
|
140
|
+
</q-table>
|
|
141
|
+
<div v-else class="text-positive text-weight-bold">✓ No Differences</div>
|
|
142
|
+
</div>
|
|
143
|
+
</q-td>
|
|
144
|
+
</q-tr>
|
|
145
|
+
</template>
|
|
146
|
+
</q-table>
|
|
147
|
+
|
|
148
|
+
</l-page>
|
|
149
|
+
</template>
|
|
@@ -64,11 +64,10 @@ const removeField = async (table) => {
|
|
|
64
64
|
ok: "Yes",
|
|
65
65
|
cancel: "No"
|
|
66
66
|
}).onOk(async () => {
|
|
67
|
-
const fields = selected[table].map((field) => field.Field);
|
|
68
67
|
try {
|
|
69
68
|
await m("lightDatabaseRemoveFields", {
|
|
70
69
|
table,
|
|
71
|
-
fields
|
|
70
|
+
fields: selected[table].map((field) => field.name)
|
|
72
71
|
});
|
|
73
72
|
light.notify({
|
|
74
73
|
type: "positive",
|
|
@@ -218,7 +217,7 @@ const truncatTable = async () => {
|
|
|
218
217
|
<q-list separator bordered>
|
|
219
218
|
<q-expansion-item :label="table.name" v-for="table in data.table" dense>
|
|
220
219
|
<div class="q-ma-sm">
|
|
221
|
-
<q-table row-key="
|
|
220
|
+
<q-table row-key="name" :rows="table.columns" :rows-per-page-options="[0]" hide-pagination flat
|
|
222
221
|
bordered selection="multiple" v-model:selected="selected[table.name]" :color="$light.color">
|
|
223
222
|
<template #top-left>
|
|
224
223
|
<q-btn icon="sym_o_add" @click="add(table.name)" round flat size="sm">
|
|
@@ -3,7 +3,7 @@ import { ref, reactive } from "vue";
|
|
|
3
3
|
import { m, q } from "#imports";
|
|
4
4
|
import { useQuasar } from "quasar";
|
|
5
5
|
import { useI18n } from "vue-i18n";
|
|
6
|
-
const
|
|
6
|
+
const $q = useQuasar();
|
|
7
7
|
const { t } = useI18n();
|
|
8
8
|
const app = await q("app", ["languages"]);
|
|
9
9
|
const splitterModel = ref(62);
|
|
@@ -23,7 +23,7 @@ const onSave = async () => {
|
|
|
23
23
|
})
|
|
24
24
|
}
|
|
25
25
|
});
|
|
26
|
-
|
|
26
|
+
$q.notify({
|
|
27
27
|
message: "Save success",
|
|
28
28
|
color: "positive",
|
|
29
29
|
icon: "check"
|
|
@@ -56,7 +56,7 @@ const onUpdateTranslate = async (value, language, name) => {
|
|
|
56
56
|
language,
|
|
57
57
|
value
|
|
58
58
|
})) {
|
|
59
|
-
|
|
59
|
+
$q.notify({
|
|
60
60
|
message: "Update success",
|
|
61
61
|
color: "positive",
|
|
62
62
|
icon: "check"
|
|
@@ -67,7 +67,7 @@ const onDelete = async (name) => {
|
|
|
67
67
|
if (await m("deleteTranslate", {
|
|
68
68
|
name
|
|
69
69
|
})) {
|
|
70
|
-
|
|
70
|
+
$q.notify({
|
|
71
71
|
message: "Delete success",
|
|
72
72
|
color: "positive",
|
|
73
73
|
icon: "check"
|
|
@@ -82,7 +82,7 @@ const onDelete = async (name) => {
|
|
|
82
82
|
<l-card>
|
|
83
83
|
<q-splitter v-model="splitterModel" style="height:680px">
|
|
84
84
|
<template #before>
|
|
85
|
-
<q-table :rows="all" flat :rows-per-page-options="[0]" :columns="columns" dense separator="cell">
|
|
85
|
+
<q-table :rows="all" flat :rows-per-page-options="[0]" :columns="columns" dense separator="cell" :bordered="false">
|
|
86
86
|
<template #body="props">
|
|
87
87
|
<q-tr :props="props">
|
|
88
88
|
<q-td key="_delete" auto-width>
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
export default _default;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useLight, model, q } from "#imports";
|
|
3
|
+
import { useQuasar } from "quasar";
|
|
4
|
+
import { ref, watch, onMounted } from "vue";
|
|
5
|
+
import { useDragAndDrop } from "@formkit/drag-and-drop/vue";
|
|
6
|
+
import { animations } from "@formkit/drag-and-drop";
|
|
7
|
+
const light = useLight();
|
|
8
|
+
const $q = useQuasar();
|
|
9
|
+
const { my } = await q({
|
|
10
|
+
my: {
|
|
11
|
+
myFavorites: {
|
|
12
|
+
my_favorite_id: true,
|
|
13
|
+
label: true,
|
|
14
|
+
path: true,
|
|
15
|
+
icon: true,
|
|
16
|
+
sequence: true
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
const [parent, rows] = useDragAndDrop(my.myFavorites, {
|
|
21
|
+
plugins: [animations()],
|
|
22
|
+
dragHandle: ".drag-handle"
|
|
23
|
+
});
|
|
24
|
+
const isUpdating = ref(false);
|
|
25
|
+
const hasChanges = ref(false);
|
|
26
|
+
watch(rows, (newRows) => {
|
|
27
|
+
let hasChanged = false;
|
|
28
|
+
for (let i = 0; i < newRows.length; i++) {
|
|
29
|
+
if (newRows[i].sequence !== i + 1) {
|
|
30
|
+
hasChanged = true;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
hasChanges.value = hasChanged;
|
|
35
|
+
}, { deep: true });
|
|
36
|
+
const updateSequence = async () => {
|
|
37
|
+
if (isUpdating.value || !hasChanges.value) return;
|
|
38
|
+
isUpdating.value = true;
|
|
39
|
+
try {
|
|
40
|
+
for (let i = 0; i < rows.value.length; i++) {
|
|
41
|
+
const item = rows.value[i];
|
|
42
|
+
await model("MyFavorite").update(item.my_favorite_id, {
|
|
43
|
+
sequence: i + 1
|
|
44
|
+
});
|
|
45
|
+
item.sequence = i + 1;
|
|
46
|
+
}
|
|
47
|
+
await light.reloadMyFavorites();
|
|
48
|
+
hasChanges.value = false;
|
|
49
|
+
$q.notify({
|
|
50
|
+
message: "Order updated",
|
|
51
|
+
color: "positive",
|
|
52
|
+
icon: "check"
|
|
53
|
+
});
|
|
54
|
+
} catch (error) {
|
|
55
|
+
$q.notify({
|
|
56
|
+
message: "Update failed: " + error.message,
|
|
57
|
+
color: "negative",
|
|
58
|
+
icon: "error"
|
|
59
|
+
});
|
|
60
|
+
} finally {
|
|
61
|
+
isUpdating.value = false;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const onSave = async (id, data) => {
|
|
65
|
+
await model("MyFavorite").update(id, data);
|
|
66
|
+
$q.notify({
|
|
67
|
+
message: "Updated successfully",
|
|
68
|
+
color: "positive",
|
|
69
|
+
icon: "check"
|
|
70
|
+
});
|
|
71
|
+
const index = rows.value.findIndex((item) => item.my_favorite_id === id);
|
|
72
|
+
if (index !== -1) {
|
|
73
|
+
Object.assign(rows.value[index], data);
|
|
74
|
+
}
|
|
75
|
+
await light.reloadMyFavorites();
|
|
76
|
+
};
|
|
77
|
+
const onRemove = async (id) => {
|
|
78
|
+
await model("MyFavorite").delete(id);
|
|
79
|
+
const index = rows.value.findIndex((item) => item.my_favorite_id === id);
|
|
80
|
+
if (index !== -1) {
|
|
81
|
+
rows.value.splice(index, 1);
|
|
82
|
+
}
|
|
83
|
+
await light.reloadMyFavorites();
|
|
84
|
+
};
|
|
85
|
+
const columns = [
|
|
86
|
+
{ name: "handler", label: "", field: "handler", align: "center" },
|
|
87
|
+
{ name: "label", label: "Label", field: "label", align: "left" },
|
|
88
|
+
{ name: "path", label: "Path", field: "path", align: "left" },
|
|
89
|
+
{ name: "icon", label: "Icon", field: "icon", align: "center" },
|
|
90
|
+
{ name: "actions", label: "Actions", field: "actions", align: "center" }
|
|
91
|
+
];
|
|
92
|
+
onMounted(() => {
|
|
93
|
+
parent.value = document.querySelector("#myfav-table .q-table__middle.scroll > table > tbody");
|
|
94
|
+
});
|
|
95
|
+
</script>
|
|
96
|
+
|
|
97
|
+
<template>
|
|
98
|
+
<div>
|
|
99
|
+
<q-table id="myfav-table" :rows="rows" :columns="columns" row-key="my_favorite_id" flat :bordered="false"
|
|
100
|
+
:rows-per-page-options="[0]">
|
|
101
|
+
|
|
102
|
+
<template #body-cell-label="props">
|
|
103
|
+
<q-td :props="props">
|
|
104
|
+
{{ props.row.label }}
|
|
105
|
+
<q-popup-edit v-model="props.row.label" v-slot="scope" buttons
|
|
106
|
+
@save="onSave(props.row.my_favorite_id, { 'label': $event })">
|
|
107
|
+
<q-input v-model="scope.value" dense autofocus counter @keyup.enter="scope.set" />
|
|
108
|
+
</q-popup-edit>
|
|
109
|
+
</q-td>
|
|
110
|
+
</template>
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
<template #body-cell-icon="props">
|
|
114
|
+
<q-td :props="props" class="text-center" style="width: 80px;">
|
|
115
|
+
<!-- Icon -->
|
|
116
|
+
<l-icon-picker v-model="props.row.icon" flat round size="sm"
|
|
117
|
+
@update:model-value="onSave(props.row.my_favorite_id, { 'icon': $event })" />
|
|
118
|
+
</q-td>
|
|
119
|
+
|
|
120
|
+
</template>
|
|
121
|
+
|
|
122
|
+
<template #body-cell-handler="props">
|
|
123
|
+
<q-td :props="props" class="drag-handle" style="cursor: move; width: 40px; text-align: center;">
|
|
124
|
+
<q-icon name="drag_handle" />
|
|
125
|
+
</q-td>
|
|
126
|
+
</template>
|
|
127
|
+
<!-- 操作列 -->
|
|
128
|
+
<template #body-cell-actions="props">
|
|
129
|
+
<q-td :props="props">
|
|
130
|
+
<q-btn icon="sym_o_delete" color="negative" flat dense @click="onRemove(props.row.my_favorite_id)"
|
|
131
|
+
:disable="saving">
|
|
132
|
+
<q-tooltip>Delete item</q-tooltip>
|
|
133
|
+
</q-btn>
|
|
134
|
+
</q-td>
|
|
135
|
+
</template>
|
|
136
|
+
|
|
137
|
+
<template #no-data>
|
|
138
|
+
<div class="text-center q-pa-md">
|
|
139
|
+
<l-icon name="sym_o_favorite_border" size="48px" class="text-grey-5" />
|
|
140
|
+
<div class="text-h6 q-mt-sm">No favorites added yet</div>
|
|
141
|
+
<div class="text-subtitle2 text-grey-6 q-mt-xs">
|
|
142
|
+
You can add frequently used features here for quick access.
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</template>
|
|
146
|
+
</q-table>
|
|
147
|
+
|
|
148
|
+
<!-- 更新按鈕 -->
|
|
149
|
+
<div v-if="hasChanges" class="q-mt-md text-center">
|
|
150
|
+
<q-btn color="primary" icon="save" label="Update Order" @click="updateSequence" :loading="isUpdating"
|
|
151
|
+
:disable="isUpdating" />
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<!-- 狀態提示 -->
|
|
155
|
+
<div v-if="isUpdating" class="q-mt-md">
|
|
156
|
+
<q-linear-progress indeterminate color="primary" />
|
|
157
|
+
<div class="text-center q-mt-sm text-grey-6">Updating order...</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</template>
|
|
161
|
+
|
|
162
|
+
<style scoped>
|
|
163
|
+
.drag-list{border-radius:8px;overflow:hidden}.drag-item{transition:all .2s}.drag-item:hover{background-color:rgba(0,0,0,.02)}.drag-handle{cursor:move;min-width:40px;padding:0 8px}.drag-handle:hover .q-icon{color:var(--q-primary)!important}:deep(.formkit-dnd-is-dragging){background-color:rgba(25,118,210,.05);box-shadow:0 4px 12px rgba(0,0,0,.15);opacity:.7;transform:scale(1.02)}:deep(.formkit-dnd-placeholder){align-items:center;background-color:rgba(25,118,210,.1);border:2px dashed var(--q-primary);border-radius:4px;display:flex;height:72px;justify-content:center;margin:2px 0}:deep(.formkit-dnd-placeholder:before){color:var(--q-primary);content:"Drop here";font-size:14px;font-weight:500}
|
|
164
|
+
</style>
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
export default _default;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
export default _default;
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed, inject } from "vue";
|
|
3
|
+
import { q, m } from "#imports";
|
|
4
|
+
import { useQuasar } from "quasar";
|
|
5
|
+
import { useI18n } from "vue-i18n";
|
|
6
|
+
const reloadMenu = inject("reloadMenu");
|
|
7
|
+
const { t } = useI18n();
|
|
8
|
+
const $q = useQuasar();
|
|
9
|
+
const { app, my } = await q({
|
|
10
|
+
app: {
|
|
11
|
+
menus: true
|
|
12
|
+
},
|
|
13
|
+
my: {
|
|
14
|
+
menu: true
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
const getUUID = () => {
|
|
18
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
|
|
19
|
+
let r = Math.random() * 16 | 0, v = c == "x" ? r : r & 3 | 8;
|
|
20
|
+
return v.toString(16);
|
|
21
|
+
}).toUpperCase();
|
|
22
|
+
};
|
|
23
|
+
const convertMenusToTree = (menus) => {
|
|
24
|
+
if (!menus) return [];
|
|
25
|
+
return menus.map((menu) => ({
|
|
26
|
+
...menu,
|
|
27
|
+
uuid: menu.uuid || getUUID(),
|
|
28
|
+
icon: menu.icon || "sym_o_menu",
|
|
29
|
+
children: menu.children ? convertMenusToTree(menu.children) : []
|
|
30
|
+
}));
|
|
31
|
+
};
|
|
32
|
+
const availableMenus = ref(convertMenusToTree(app.menus));
|
|
33
|
+
const userMenus = ref([
|
|
34
|
+
{
|
|
35
|
+
label: "[My Menu]",
|
|
36
|
+
uuid: "ROOT",
|
|
37
|
+
children: my.menu,
|
|
38
|
+
type: "root"
|
|
39
|
+
}
|
|
40
|
+
]);
|
|
41
|
+
const selectedNode = ref("ROOT");
|
|
42
|
+
const splitterModel = ref(30);
|
|
43
|
+
const userMenuTree = ref(null);
|
|
44
|
+
const currentNodeDetail = computed(() => {
|
|
45
|
+
if (selectedNode.value === "ROOT") return userMenus.value[0];
|
|
46
|
+
if (!userMenuTree.value) return null;
|
|
47
|
+
return userMenuTree.value.getNodeByKey(selectedNode.value);
|
|
48
|
+
});
|
|
49
|
+
const cloneMenuWithChildren = (menu, parentUuid) => {
|
|
50
|
+
const clonedMenu = {
|
|
51
|
+
...menu,
|
|
52
|
+
uuid: getUUID(),
|
|
53
|
+
parent: parentUuid,
|
|
54
|
+
children: []
|
|
55
|
+
};
|
|
56
|
+
if (menu.children && menu.children.length > 0) {
|
|
57
|
+
clonedMenu.children = menu.children.map(
|
|
58
|
+
(child) => cloneMenuWithChildren(child, clonedMenu.uuid)
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return clonedMenu;
|
|
62
|
+
};
|
|
63
|
+
const addMenuToCustom = (menu) => {
|
|
64
|
+
const targetNode = currentNodeDetail.value;
|
|
65
|
+
if (!targetNode) return;
|
|
66
|
+
const newMenuItem = cloneMenuWithChildren(menu, targetNode.uuid);
|
|
67
|
+
if (!targetNode.children) targetNode.children = [];
|
|
68
|
+
targetNode.children.push(newMenuItem);
|
|
69
|
+
if (userMenuTree.value && targetNode.uuid !== "ROOT") {
|
|
70
|
+
userMenuTree.value.setExpanded(selectedNode.value, true);
|
|
71
|
+
}
|
|
72
|
+
if (newMenuItem.children && newMenuItem.children.length > 0) {
|
|
73
|
+
setTimeout(() => {
|
|
74
|
+
if (userMenuTree.value) {
|
|
75
|
+
userMenuTree.value.setExpanded(newMenuItem.uuid, true);
|
|
76
|
+
}
|
|
77
|
+
}, 100);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const createNewFolder = () => {
|
|
81
|
+
$q.dialog({
|
|
82
|
+
title: "Create New Folder",
|
|
83
|
+
message: "Please enter folder name",
|
|
84
|
+
prompt: {
|
|
85
|
+
model: "",
|
|
86
|
+
type: "text"
|
|
87
|
+
},
|
|
88
|
+
cancel: true,
|
|
89
|
+
persistent: true
|
|
90
|
+
}).onOk((folderName) => {
|
|
91
|
+
if (folderName.trim() === "") return;
|
|
92
|
+
const targetNode = currentNodeDetail.value;
|
|
93
|
+
if (!targetNode) return;
|
|
94
|
+
const newFolder = {
|
|
95
|
+
label: folderName,
|
|
96
|
+
uuid: getUUID(),
|
|
97
|
+
parent: targetNode.uuid,
|
|
98
|
+
children: [],
|
|
99
|
+
icon: "sym_o_folder"
|
|
100
|
+
};
|
|
101
|
+
if (!targetNode.children) targetNode.children = [];
|
|
102
|
+
targetNode.children.push(newFolder);
|
|
103
|
+
if (userMenuTree.value && targetNode.uuid !== "ROOT") {
|
|
104
|
+
userMenuTree.value.setExpanded(selectedNode.value, true);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
const createNewSubItem = () => {
|
|
109
|
+
$q.dialog({
|
|
110
|
+
title: "Create New Sub Item",
|
|
111
|
+
message: "Please enter item name",
|
|
112
|
+
prompt: {
|
|
113
|
+
model: "",
|
|
114
|
+
type: "text"
|
|
115
|
+
},
|
|
116
|
+
cancel: true,
|
|
117
|
+
persistent: true
|
|
118
|
+
}).onOk((itemName) => {
|
|
119
|
+
if (itemName.trim() === "") return;
|
|
120
|
+
const targetNode = currentNodeDetail.value;
|
|
121
|
+
if (!targetNode) return;
|
|
122
|
+
const newItem = {
|
|
123
|
+
label: itemName,
|
|
124
|
+
uuid: getUUID(),
|
|
125
|
+
parent: targetNode.uuid,
|
|
126
|
+
children: [],
|
|
127
|
+
icon: "sym_o_menu",
|
|
128
|
+
to: ""
|
|
129
|
+
};
|
|
130
|
+
if (!targetNode.children) targetNode.children = [];
|
|
131
|
+
targetNode.children.push(newItem);
|
|
132
|
+
if (userMenuTree.value && targetNode.uuid !== "ROOT") {
|
|
133
|
+
userMenuTree.value.setExpanded(selectedNode.value, true);
|
|
134
|
+
}
|
|
135
|
+
selectedNode.value = newItem.uuid;
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
const addSeparator = () => {
|
|
139
|
+
const targetNode = currentNodeDetail.value;
|
|
140
|
+
if (!targetNode) return;
|
|
141
|
+
const separator = {
|
|
142
|
+
label: "[Separator]",
|
|
143
|
+
uuid: getUUID(),
|
|
144
|
+
parent: targetNode.uuid,
|
|
145
|
+
children: [],
|
|
146
|
+
type: "separator",
|
|
147
|
+
spaced: true
|
|
148
|
+
};
|
|
149
|
+
if (!targetNode.children) targetNode.children = [];
|
|
150
|
+
targetNode.children.push(separator);
|
|
151
|
+
};
|
|
152
|
+
const removeMenuItem = (node) => {
|
|
153
|
+
if (node.type === "root") return;
|
|
154
|
+
$q.dialog({
|
|
155
|
+
title: "Confirm Delete",
|
|
156
|
+
message: `Are you sure you want to delete "${node.label}"?`,
|
|
157
|
+
cancel: true,
|
|
158
|
+
persistent: true
|
|
159
|
+
}).onOk(() => {
|
|
160
|
+
const parent = getParentNode(node);
|
|
161
|
+
if (parent && parent.children) {
|
|
162
|
+
parent.children = parent.children.filter((item) => item.uuid !== node.uuid);
|
|
163
|
+
selectedNode.value = parent.uuid;
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
const getParentNode = (node) => {
|
|
168
|
+
if (!node.parent || node.parent === "ROOT") {
|
|
169
|
+
return userMenus.value[0];
|
|
170
|
+
}
|
|
171
|
+
return userMenuTree.value.getNodeByKey(node.parent);
|
|
172
|
+
};
|
|
173
|
+
const moveUp = (node) => {
|
|
174
|
+
const parent = getParentNode(node);
|
|
175
|
+
if (!parent || !parent.children) return;
|
|
176
|
+
const index = parent.children.findIndex((item) => item.uuid === node.uuid);
|
|
177
|
+
if (index > 0) {
|
|
178
|
+
const temp = parent.children[index - 1];
|
|
179
|
+
parent.children[index - 1] = node;
|
|
180
|
+
parent.children[index] = temp;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
const moveDown = (node) => {
|
|
184
|
+
const parent = getParentNode(node);
|
|
185
|
+
if (!parent || !parent.children) return;
|
|
186
|
+
const index = parent.children.findIndex((item) => item.uuid === node.uuid);
|
|
187
|
+
if (index < parent.children.length - 1) {
|
|
188
|
+
const temp = parent.children[index + 1];
|
|
189
|
+
parent.children[index + 1] = node;
|
|
190
|
+
parent.children[index] = temp;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
const showMoveDialog = ref(false);
|
|
194
|
+
const moveTarget = ref(null);
|
|
195
|
+
const moveToGroup = () => {
|
|
196
|
+
showMoveDialog.value = true;
|
|
197
|
+
moveTarget.value = null;
|
|
198
|
+
};
|
|
199
|
+
const confirmMove = () => {
|
|
200
|
+
if (!moveTarget.value || !currentNodeDetail.value) return;
|
|
201
|
+
const nodeToMove = JSON.parse(JSON.stringify(currentNodeDetail.value));
|
|
202
|
+
removeMenuItem(currentNodeDetail.value);
|
|
203
|
+
nodeToMove.parent = moveTarget.value;
|
|
204
|
+
const targetNode = moveTarget.value === "ROOT" ? userMenus.value[0] : userMenuTree.value.getNodeByKey(moveTarget.value);
|
|
205
|
+
if (!targetNode.children) targetNode.children = [];
|
|
206
|
+
targetNode.children.push(nodeToMove);
|
|
207
|
+
if (userMenuTree.value && moveTarget.value !== "ROOT") {
|
|
208
|
+
userMenuTree.value.setExpanded(moveTarget.value, true);
|
|
209
|
+
}
|
|
210
|
+
showMoveDialog.value = false;
|
|
211
|
+
selectedNode.value = nodeToMove.uuid;
|
|
212
|
+
};
|
|
213
|
+
const saveCustomMenus = async () => {
|
|
214
|
+
console.log("User custom menu structure:", userMenus.value[0].children);
|
|
215
|
+
try {
|
|
216
|
+
await m("updateMyMenu", {
|
|
217
|
+
menu: userMenus.value[0].children
|
|
218
|
+
});
|
|
219
|
+
if (reloadMenu) {
|
|
220
|
+
reloadMenu();
|
|
221
|
+
}
|
|
222
|
+
} catch (e) {
|
|
223
|
+
console.error("Failed to save custom menu:", e);
|
|
224
|
+
$q.notify({
|
|
225
|
+
message: "Failed to save custom menu",
|
|
226
|
+
color: "negative",
|
|
227
|
+
icon: "error",
|
|
228
|
+
position: "top",
|
|
229
|
+
timeout: 2e3
|
|
230
|
+
});
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
$q.notify({
|
|
234
|
+
message: "Menu structure saved successfully",
|
|
235
|
+
color: "positive",
|
|
236
|
+
icon: "check",
|
|
237
|
+
position: "top",
|
|
238
|
+
timeout: 2e3
|
|
239
|
+
});
|
|
240
|
+
};
|
|
241
|
+
const resetMenus = () => {
|
|
242
|
+
$q.dialog({
|
|
243
|
+
title: "Confirm Reset",
|
|
244
|
+
message: "This will clear all custom menu settings. Are you sure you want to reset?",
|
|
245
|
+
cancel: true,
|
|
246
|
+
persistent: true
|
|
247
|
+
}).onOk(() => {
|
|
248
|
+
userMenus.value[0].children = [];
|
|
249
|
+
selectedNode.value = "ROOT";
|
|
250
|
+
});
|
|
251
|
+
};
|
|
252
|
+
</script>
|
|
253
|
+
|
|
254
|
+
<template>
|
|
255
|
+
<div>
|
|
256
|
+
<!-- Move to group dialog -->
|
|
257
|
+
<q-dialog v-model="showMoveDialog">
|
|
258
|
+
<l-card>
|
|
259
|
+
<q-card-section>
|
|
260
|
+
<div class="text-h6">Select Move Target</div>
|
|
261
|
+
<q-tree :nodes="userMenus" :selected-color="$light.color" default-expand-all
|
|
262
|
+
v-model:selected="moveTarget" node-key="uuid" />
|
|
263
|
+
</q-card-section>
|
|
264
|
+
<q-card-actions align="right">
|
|
265
|
+
<q-btn flat label="Cancel" :color="$light.color" v-close-popup />
|
|
266
|
+
<q-btn flat label="Move" :color="$light.color" @click="confirmMove" />
|
|
267
|
+
</q-card-actions>
|
|
268
|
+
</l-card>
|
|
269
|
+
</q-dialog>
|
|
270
|
+
|
|
271
|
+
<l-card>
|
|
272
|
+
<q-card-actions class="q-gutter-sm">
|
|
273
|
+
<l-btn @click="saveCustomMenus" label="Save Custom Menu" icon="sym_o_save" />
|
|
274
|
+
<l-btn @click="resetMenus" label="Reset Menu" icon="sym_o_refresh" />
|
|
275
|
+
</q-card-actions>
|
|
276
|
+
|
|
277
|
+
<q-splitter v-model="splitterModel" style="height:680px">
|
|
278
|
+
<template #before>
|
|
279
|
+
<div class="q-pa-md">
|
|
280
|
+
<div class="text-h6 q-mb-md">Available Menu Items</div>
|
|
281
|
+
<q-tree :nodes="availableMenus" :selected-color="$light.color" default-expand-all
|
|
282
|
+
node-key="uuid">
|
|
283
|
+
<template v-slot:default-header="prop">
|
|
284
|
+
<div class="row items-center full-width">
|
|
285
|
+
<q-icon :name="prop.node.icon || 'sym_o_menu'" class="q-mr-sm" size="sm" />
|
|
286
|
+
<span>{{ prop.node.label }}</span>
|
|
287
|
+
<q-space />
|
|
288
|
+
<small v-if="prop.node.to" class="text-grey-6 q-mr-sm">{{ prop.node.to }}</small>
|
|
289
|
+
<q-btn flat round dense size="sm" icon="sym_o_add" color="positive"
|
|
290
|
+
@click.stop="addMenuToCustom(prop.node)">
|
|
291
|
+
<q-tooltip>Add to My Menu</q-tooltip>
|
|
292
|
+
</q-btn>
|
|
293
|
+
</div>
|
|
294
|
+
</template>
|
|
295
|
+
</q-tree>
|
|
296
|
+
</div>
|
|
297
|
+
</template>
|
|
298
|
+
|
|
299
|
+
<template #after>
|
|
300
|
+
<div class="q-pa-md">
|
|
301
|
+
<div class="text-h6 q-mb-md">My Custom Menu</div>
|
|
302
|
+
|
|
303
|
+
<!-- Action buttons -->
|
|
304
|
+
<q-card-actions class="q-mb-md q-gutter-xs" v-if="currentNodeDetail">
|
|
305
|
+
<template v-if="currentNodeDetail.type === 'root'">
|
|
306
|
+
<q-btn-dropdown v-bind="$light.getButtonProps({ label: 'Add' })" icon="sym_o_add"
|
|
307
|
+
size="sm">
|
|
308
|
+
<q-list>
|
|
309
|
+
<q-item clickable v-close-popup @click="createNewFolder">
|
|
310
|
+
<q-item-section avatar>
|
|
311
|
+
<q-icon name="sym_o_folder" color="orange" />
|
|
312
|
+
</q-item-section>
|
|
313
|
+
<q-item-section>
|
|
314
|
+
<q-item-label>Add Folder</q-item-label>
|
|
315
|
+
</q-item-section>
|
|
316
|
+
</q-item>
|
|
317
|
+
<q-item clickable v-close-popup @click="createNewSubItem">
|
|
318
|
+
<q-item-section avatar>
|
|
319
|
+
<q-icon name="sym_o_menu" />
|
|
320
|
+
</q-item-section>
|
|
321
|
+
<q-item-section>
|
|
322
|
+
<q-item-label>Add Sub Item</q-item-label>
|
|
323
|
+
</q-item-section>
|
|
324
|
+
</q-item>
|
|
325
|
+
<q-item clickable v-close-popup @click="addSeparator">
|
|
326
|
+
<q-item-section avatar>
|
|
327
|
+
<q-icon name="sym_o_more_horiz" color="grey" />
|
|
328
|
+
</q-item-section>
|
|
329
|
+
<q-item-section>
|
|
330
|
+
<q-item-label>Add Separator</q-item-label>
|
|
331
|
+
</q-item-section>
|
|
332
|
+
</q-item>
|
|
333
|
+
</q-list>
|
|
334
|
+
</q-btn-dropdown>
|
|
335
|
+
</template>
|
|
336
|
+
<template v-else>
|
|
337
|
+
<q-btn-dropdown v-bind="$light.getButtonProps({ label: 'Add' })" icon="sym_o_add"
|
|
338
|
+
size="sm">
|
|
339
|
+
<q-list>
|
|
340
|
+
<q-item clickable v-close-popup @click="createNewFolder">
|
|
341
|
+
<q-item-section avatar>
|
|
342
|
+
<q-icon name="sym_o_folder" color="orange" />
|
|
343
|
+
</q-item-section>
|
|
344
|
+
<q-item-section>
|
|
345
|
+
<q-item-label>Add Folder</q-item-label>
|
|
346
|
+
</q-item-section>
|
|
347
|
+
</q-item>
|
|
348
|
+
<q-item clickable v-close-popup @click="createNewSubItem">
|
|
349
|
+
<q-item-section avatar>
|
|
350
|
+
<q-icon name="sym_o_menu" />
|
|
351
|
+
</q-item-section>
|
|
352
|
+
<q-item-section>
|
|
353
|
+
<q-item-label>Add Sub Item</q-item-label>
|
|
354
|
+
</q-item-section>
|
|
355
|
+
</q-item>
|
|
356
|
+
<q-item clickable v-close-popup @click="addSeparator">
|
|
357
|
+
<q-item-section avatar>
|
|
358
|
+
<q-icon name="sym_o_more_horiz" color="grey" />
|
|
359
|
+
</q-item-section>
|
|
360
|
+
<q-item-section>
|
|
361
|
+
<q-item-label>Add Separator</q-item-label>
|
|
362
|
+
</q-item-section>
|
|
363
|
+
</q-item>
|
|
364
|
+
</q-list>
|
|
365
|
+
</q-btn-dropdown>
|
|
366
|
+
|
|
367
|
+
<l-btn @click="removeMenuItem(currentNodeDetail)" label="Delete" icon="sym_o_delete"
|
|
368
|
+
size="sm" color="negative" />
|
|
369
|
+
<l-btn @click="moveUp(currentNodeDetail)" label="Move Up" icon="sym_o_arrow_upward"
|
|
370
|
+
size="sm" />
|
|
371
|
+
<l-btn @click="moveDown(currentNodeDetail)" label="Move Down" icon="sym_o_arrow_downward"
|
|
372
|
+
size="sm" />
|
|
373
|
+
<l-btn @click="moveToGroup" label="Move to..." icon="sym_o_drive_file_move" size="sm" />
|
|
374
|
+
</template>
|
|
375
|
+
</q-card-actions>
|
|
376
|
+
|
|
377
|
+
<!-- Menu tree -->
|
|
378
|
+
<q-tree :nodes="userMenus" :selected-color="$light.color" default-expand-all
|
|
379
|
+
v-model:selected="selectedNode" node-key="uuid" ref="userMenuTree">
|
|
380
|
+
<template v-slot:default-header="prop">
|
|
381
|
+
<div class="row items-center full-width">
|
|
382
|
+
<q-icon :name="prop.node.type === 'separator' ? 'sym_o_more_horiz' :
|
|
383
|
+
prop.node.type === 'root' ? 'sym_o_account_tree' :
|
|
384
|
+
prop.node.icon || 'sym_o_menu'" class="q-mr-sm" size="sm" :color="prop.node.type === 'separator' ? 'grey' :
|
|
385
|
+
prop.node.type === 'root' ? 'primary' : undefined" />
|
|
386
|
+
<span :class="{
|
|
387
|
+
'text-grey-6': prop.node.type === 'separator',
|
|
388
|
+
'text-primary': prop.node.type === 'root'
|
|
389
|
+
}">
|
|
390
|
+
{{ prop.node.label }}
|
|
391
|
+
</span>
|
|
392
|
+
<q-space />
|
|
393
|
+
<small v-if="prop.node.to" class="text-grey-6">{{ prop.node.to }}</small>
|
|
394
|
+
</div>
|
|
395
|
+
</template>
|
|
396
|
+
</q-tree>
|
|
397
|
+
|
|
398
|
+
<!-- Current selected node detail settings -->
|
|
399
|
+
<div v-if="currentNodeDetail && currentNodeDetail.type !== 'root'" class="q-mt-lg">
|
|
400
|
+
<q-separator class="q-mb-md" />
|
|
401
|
+
<div class="text-subtitle1 q-mb-md">Menu Settings</div>
|
|
402
|
+
|
|
403
|
+
<template v-if="currentNodeDetail.type === 'separator'">
|
|
404
|
+
<div class="q-gutter-md">
|
|
405
|
+
<l-checkbox label="Spaced Separator" v-model="currentNodeDetail.spaced" />
|
|
406
|
+
</div>
|
|
407
|
+
</template>
|
|
408
|
+
|
|
409
|
+
<template v-else>
|
|
410
|
+
<div class="q-gutter-md">
|
|
411
|
+
<l-input label="Label" v-model="currentNodeDetail.label" />
|
|
412
|
+
<l-input label="Route" v-model="currentNodeDetail.to" />
|
|
413
|
+
<l-icon-picker label="Icon" v-model="currentNodeDetail.icon" />
|
|
414
|
+
</div>
|
|
415
|
+
</template>
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
418
|
+
</template>
|
|
419
|
+
</q-splitter>
|
|
420
|
+
</l-card>
|
|
421
|
+
</div>
|
|
422
|
+
</template>
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
export default _default;
|
|
@@ -24,9 +24,11 @@ const route = useRoute();
|
|
|
24
24
|
exact />
|
|
25
25
|
<q-route-tab name="openid" icon="sym_o_key" :label="$t('Open ID')" to="/User/setting/open_id"
|
|
26
26
|
exact />
|
|
27
|
-
<q-route-tab name="
|
|
28
|
-
to="/User/setting/
|
|
27
|
+
<q-route-tab name="fav" icon="sym_o_favorite" :label="$t('Favorite')"
|
|
28
|
+
to="/User/setting/favorite" exact />
|
|
29
29
|
|
|
30
|
+
<q-route-tab name="menu" icon="sym_o_menu" :label="$t('Menu')"
|
|
31
|
+
to="/User/setting/menu" exact />
|
|
30
32
|
</q-tabs>
|
|
31
33
|
</template>
|
|
32
34
|
|
package/package.json
CHANGED
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import { useLight, model, q } from "#imports";
|
|
3
|
-
import { useQuasar } from "quasar";
|
|
4
|
-
import { ref, watch } from "vue";
|
|
5
|
-
import { useDragAndDrop } from "@formkit/drag-and-drop/vue";
|
|
6
|
-
import { animations } from "@formkit/drag-and-drop";
|
|
7
|
-
const light = useLight();
|
|
8
|
-
const $q = useQuasar();
|
|
9
|
-
const { my } = await q({
|
|
10
|
-
my: {
|
|
11
|
-
myFavorites: {
|
|
12
|
-
my_favorite_id: true,
|
|
13
|
-
label: true,
|
|
14
|
-
path: true,
|
|
15
|
-
icon: true,
|
|
16
|
-
sequence: true
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
});
|
|
20
|
-
const [dragParent, rows] = useDragAndDrop(my.myFavorites, {
|
|
21
|
-
plugins: [animations()],
|
|
22
|
-
dragHandle: ".drag-handle"
|
|
23
|
-
});
|
|
24
|
-
const isUpdating = ref(false);
|
|
25
|
-
watch(rows, async (newRows) => {
|
|
26
|
-
if (isUpdating.value) return;
|
|
27
|
-
let hasChanged = false;
|
|
28
|
-
for (let i = 0; i < newRows.length; i++) {
|
|
29
|
-
if (newRows[i].sequence !== i + 1) {
|
|
30
|
-
hasChanged = true;
|
|
31
|
-
break;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
if (!hasChanged) return;
|
|
35
|
-
isUpdating.value = true;
|
|
36
|
-
try {
|
|
37
|
-
for (let i = 0; i < newRows.length; i++) {
|
|
38
|
-
const item = newRows[i];
|
|
39
|
-
await model("MyFavorite").update(item.my_favorite_id, {
|
|
40
|
-
sequence: i + 1
|
|
41
|
-
});
|
|
42
|
-
item.sequence = i + 1;
|
|
43
|
-
}
|
|
44
|
-
await light.reloadMyFavorites();
|
|
45
|
-
$q.notify({
|
|
46
|
-
message: "\u9806\u5E8F\u5DF2\u66F4\u65B0",
|
|
47
|
-
color: "positive",
|
|
48
|
-
icon: "check"
|
|
49
|
-
});
|
|
50
|
-
} catch (error) {
|
|
51
|
-
$q.notify({
|
|
52
|
-
message: "\u66F4\u65B0\u5931\u6557: " + error.message,
|
|
53
|
-
color: "negative",
|
|
54
|
-
icon: "error"
|
|
55
|
-
});
|
|
56
|
-
} finally {
|
|
57
|
-
isUpdating.value = false;
|
|
58
|
-
}
|
|
59
|
-
}, { deep: true });
|
|
60
|
-
const onSave = async (id, data) => {
|
|
61
|
-
await model("MyFavorite").update(id, data);
|
|
62
|
-
$q.notify({
|
|
63
|
-
message: "Updated successfully",
|
|
64
|
-
color: "positive",
|
|
65
|
-
icon: "check"
|
|
66
|
-
});
|
|
67
|
-
const index = rows.value.findIndex((item) => item.my_favorite_id === id);
|
|
68
|
-
if (index !== -1) {
|
|
69
|
-
Object.assign(rows.value[index], data);
|
|
70
|
-
}
|
|
71
|
-
await light.reloadMyFavorites();
|
|
72
|
-
};
|
|
73
|
-
const onRemove = async (id) => {
|
|
74
|
-
await model("MyFavorite").delete(id);
|
|
75
|
-
const index = rows.value.findIndex((item) => item.my_favorite_id === id);
|
|
76
|
-
if (index !== -1) {
|
|
77
|
-
rows.value.splice(index, 1);
|
|
78
|
-
}
|
|
79
|
-
await light.reloadMyFavorites();
|
|
80
|
-
};
|
|
81
|
-
</script>
|
|
82
|
-
|
|
83
|
-
<template>
|
|
84
|
-
<div>
|
|
85
|
-
<q-list ref="dragParent" bordered separator class="drag-list">
|
|
86
|
-
<q-item
|
|
87
|
-
v-for="(row, index) in rows"
|
|
88
|
-
:key="row.my_favorite_id"
|
|
89
|
-
class="drag-item"
|
|
90
|
-
clickable
|
|
91
|
-
>
|
|
92
|
-
<!-- 拖拽手柄 -->
|
|
93
|
-
<q-item-section avatar class="drag-handle">
|
|
94
|
-
<q-icon name="drag_handle" class="cursor-move" color="grey-6" />
|
|
95
|
-
</q-item-section>
|
|
96
|
-
|
|
97
|
-
<!-- 主要內容 -->
|
|
98
|
-
<q-item-section>
|
|
99
|
-
<q-item-label>
|
|
100
|
-
<div class="row items-center q-gutter-sm">
|
|
101
|
-
<!-- Icon -->
|
|
102
|
-
<l-icon-picker
|
|
103
|
-
v-model="row.icon"
|
|
104
|
-
flat
|
|
105
|
-
round
|
|
106
|
-
size="sm"
|
|
107
|
-
@update:model-value="onSave(row.my_favorite_id, { 'icon': $event })"
|
|
108
|
-
/>
|
|
109
|
-
|
|
110
|
-
<!-- Label -->
|
|
111
|
-
<span class="text-weight-medium">
|
|
112
|
-
{{ row.label }}
|
|
113
|
-
<q-popup-edit v-model="row.label" #default="scope" buttons
|
|
114
|
-
@save="onSave(row.my_favorite_id, { 'label': $event })">
|
|
115
|
-
<q-input v-model="scope.value" dense autofocus counter @keyup.enter="scope.set" />
|
|
116
|
-
</q-popup-edit>
|
|
117
|
-
</span>
|
|
118
|
-
</div>
|
|
119
|
-
</q-item-label>
|
|
120
|
-
|
|
121
|
-
<q-item-label caption class="text-grey-7">
|
|
122
|
-
{{ row.path }}
|
|
123
|
-
<q-popup-edit v-model="row.path" #default="scope" buttons
|
|
124
|
-
@save="onSave(row.my_favorite_id, { 'path': $event })">
|
|
125
|
-
<q-input v-model="scope.value" dense autofocus counter @keyup.enter="scope.set" />
|
|
126
|
-
</q-popup-edit>
|
|
127
|
-
</q-item-label>
|
|
128
|
-
</q-item-section>
|
|
129
|
-
|
|
130
|
-
<!-- 操作按鈕 -->
|
|
131
|
-
<q-item-section side>
|
|
132
|
-
<q-btn
|
|
133
|
-
flat
|
|
134
|
-
round
|
|
135
|
-
icon="sym_o_delete"
|
|
136
|
-
size="sm"
|
|
137
|
-
color="negative"
|
|
138
|
-
@click="onRemove(row.my_favorite_id)"
|
|
139
|
-
>
|
|
140
|
-
<q-tooltip>刪除</q-tooltip>
|
|
141
|
-
</q-btn>
|
|
142
|
-
</q-item-section>
|
|
143
|
-
</q-item>
|
|
144
|
-
</q-list>
|
|
145
|
-
|
|
146
|
-
<!-- 狀態提示 -->
|
|
147
|
-
<div v-if="isUpdating" class="q-mt-md">
|
|
148
|
-
<q-linear-progress indeterminate color="primary" />
|
|
149
|
-
<div class="text-center q-mt-sm text-grey-6">正在更新順序...</div>
|
|
150
|
-
</div>
|
|
151
|
-
</div>
|
|
152
|
-
</template>
|
|
153
|
-
|
|
154
|
-
<style scoped>
|
|
155
|
-
.drag-list{border-radius:8px;overflow:hidden}.drag-item{transition:all .2s}.drag-item:hover{background-color:rgba(0,0,0,.02)}.drag-handle{cursor:move;min-width:40px;padding:0 8px}.drag-handle:hover .q-icon{color:var(--q-primary)!important}:deep(.formkit-dnd-is-dragging){background-color:rgba(25,118,210,.05);box-shadow:0 4px 12px rgba(0,0,0,.15);opacity:.7;transform:scale(1.02)}:deep(.formkit-dnd-placeholder){align-items:center;background-color:rgba(25,118,210,.1);border:2px dashed var(--q-primary);border-radius:4px;display:flex;height:72px;justify-content:center;margin:2px 0}:deep(.formkit-dnd-placeholder:before){color:var(--q-primary);content:"放置在此處";font-size:14px;font-weight:500}
|
|
156
|
-
</style>
|
/package/dist/runtime/pages/{User/setting/my_favorite.d.vue.ts → System/database/check.d.vue.ts}
RENAMED
|
File without changes
|
/package/dist/runtime/pages/{User/setting/my_favorite.vue.d.ts → System/database/check.vue.d.ts}
RENAMED
|
File without changes
|