@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "light",
3
3
  "configKey": "light",
4
- "version": "1.50.0",
4
+ "version": "1.51.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
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-my_favorite",
230
- path: "my_favorite",
231
- file: "runtime/pages/User/setting/my_favorite.vue"
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: {}, __VLS_267: {}, __VLS_273: {};
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 __VLS_267) => any;
7
+ 'page-top'?: (props: typeof __VLS_271) => any;
8
8
  } & {
9
- 'page-bottom'?: (props: typeof __VLS_273) => any;
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, onBeforeUnmount } from "vue";
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("app", ["menus"]);
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: {}, __VLS_267: {}, __VLS_273: {};
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 __VLS_267) => any;
7
+ 'page-top'?: (props: typeof __VLS_271) => any;
8
8
  } & {
9
- 'page-bottom'?: (props: typeof __VLS_273) => any;
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)" :class="getCellClass(col, props.row)"><template
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="Field" :rows="table.columns" :rows-per-page-options="[0]" hide-pagination flat
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 quasar = useQuasar();
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
- quasar.notify({
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
- quasar.notify({
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
- quasar.notify({
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;
@@ -1,5 +1,4 @@
1
1
  <script setup>
2
- import { reset } from "@formkit/core";
3
2
  import { q, m, notify } from "#imports";
4
3
  const { my } = await q({
5
4
  my: {
@@ -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="myfav" icon="sym_o_favorite" :label="$t('My favorite')"
28
- to="/User/setting/my_favorite" exact />
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@hostlink/nuxt-light",
3
- "version": "1.50.0",
3
+ "version": "1.51.0",
4
4
  "description": "HostLink Nuxt Light Framework",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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>