@hostlink/nuxt-light 1.67.8 → 1.69.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.
Files changed (47) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +106 -287
  3. package/dist/runtime/components/L/AppMain.d.vue.ts +4 -4
  4. package/dist/runtime/components/L/AppMain.vue +3 -0
  5. package/dist/runtime/components/L/AppMain.vue.d.ts +4 -4
  6. package/dist/runtime/components/L/NotificationBell.vue +187 -0
  7. package/dist/runtime/components/L/System/Setting/mail.vue +15 -8
  8. package/dist/runtime/components/L/System/Setting/modules.d.vue.ts +1 -0
  9. package/dist/runtime/components/L/System/Setting/modules.vue +6 -2
  10. package/dist/runtime/components/L/System/Setting/modules.vue.d.ts +1 -0
  11. package/dist/runtime/components/L/Table.d.vue.ts +12 -6
  12. package/dist/runtime/components/L/Table.vue +7 -1
  13. package/dist/runtime/components/L/Table.vue.d.ts +12 -6
  14. package/dist/runtime/models/Notification.d.ts +2 -0
  15. package/dist/runtime/models/Notification.js +52 -0
  16. package/dist/runtime/models/SystemValue.js +3 -1
  17. package/dist/runtime/pages/Notification/index.vue +55 -0
  18. package/dist/runtime/pages/System/view_as.vue +1 -1
  19. package/dist/runtime/pages/User/[user_id]/update-role.d.vue.ts +3 -0
  20. package/dist/runtime/pages/User/[user_id]/update-role.vue.d.ts +3 -0
  21. package/dist/runtime/pages/User/[user_id]/view.d.vue.ts +3 -0
  22. package/dist/runtime/pages/User/[user_id]/view.vue.d.ts +3 -0
  23. package/dist/runtime/pages/page_not_found.d.vue.ts +3 -0
  24. package/dist/runtime/pages/page_not_found.vue +26 -0
  25. package/dist/runtime/pages/page_not_found.vue.d.ts +3 -0
  26. package/package.json +3 -2
  27. /package/dist/runtime/{pages/EventLog/_eventlog_id/view.d.vue.ts → components/L/NotificationBell.d.vue.ts} +0 -0
  28. /package/dist/runtime/{pages/EventLog/_eventlog_id/view.vue.d.ts → components/L/NotificationBell.vue.d.ts} +0 -0
  29. /package/dist/runtime/pages/{User/_user_id → EventLog/[eventlog_id]}/view.d.vue.ts +0 -0
  30. /package/dist/runtime/pages/EventLog/{_eventlog_id → [eventlog_id]}/view.vue +0 -0
  31. /package/dist/runtime/pages/{User/_user_id → EventLog/[eventlog_id]}/view.vue.d.ts +0 -0
  32. /package/dist/runtime/pages/{Role/_name/update-child.d.vue.ts → Notification/index.d.vue.ts} +0 -0
  33. /package/dist/runtime/pages/{Role/_name/update-child.vue.d.ts → Notification/index.vue.d.ts} +0 -0
  34. /package/dist/runtime/pages/{SystemValue/_systemvalue_id/edit.d.vue.ts → Role/[name]/update-child.d.vue.ts} +0 -0
  35. /package/dist/runtime/pages/Role/{_name → [name]}/update-child.vue +0 -0
  36. /package/dist/runtime/pages/{SystemValue/_systemvalue_id/edit.vue.d.ts → Role/[name]/update-child.vue.d.ts} +0 -0
  37. /package/dist/runtime/pages/{User/_user_id → SystemValue/[systemvalue_id]}/edit.d.vue.ts +0 -0
  38. /package/dist/runtime/pages/SystemValue/{_systemvalue_id → [systemvalue_id]}/edit.vue +0 -0
  39. /package/dist/runtime/pages/{User/_user_id → SystemValue/[systemvalue_id]}/edit.vue.d.ts +0 -0
  40. /package/dist/runtime/pages/User/{_user_id → [user_id]}/change-password.d.vue.ts +0 -0
  41. /package/dist/runtime/pages/User/{_user_id → [user_id]}/change-password.vue +0 -0
  42. /package/dist/runtime/pages/User/{_user_id → [user_id]}/change-password.vue.d.ts +0 -0
  43. /package/dist/runtime/pages/User/{_user_id/update-role.d.vue.ts → [user_id]/edit.d.vue.ts} +0 -0
  44. /package/dist/runtime/pages/User/{_user_id → [user_id]}/edit.vue +0 -0
  45. /package/dist/runtime/pages/User/{_user_id/update-role.vue.d.ts → [user_id]/edit.vue.d.ts} +0 -0
  46. /package/dist/runtime/pages/User/{_user_id → [user_id]}/update-role.vue +0 -0
  47. /package/dist/runtime/pages/User/{_user_id → [user_id]}/view.vue +0 -0
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "light",
3
3
  "configKey": "light",
4
- "version": "1.67.8",
4
+ "version": "1.69.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,283 +1,100 @@
1
1
  import { defineNuxtModule, createResolver, extendPages, addImports, addComponentsDir, addImportsDir, resolveFiles, addPluginTemplate, addPlugin } from '@nuxt/kit';
2
+ import { readdirSync } from 'node:fs';
3
+ import { basename, join, relative, dirname } from 'node:path';
2
4
 
3
- const routes = [
4
- {
5
- name: "CustomField",
6
- path: "/CustomField",
7
- file: "runtime/pages/CustomField/index.vue"
8
- },
9
- {
10
- name: "CustomField-custom_field_id-edit",
11
- path: "/CustomField/:custom_field_id/edit",
12
- file: "runtime/pages/CustomField/[custom_field_id]/edit.vue"
13
- },
14
- {
15
- name: "EventLog",
16
- path: "/EventLog",
17
- file: "runtime/pages/EventLog/index.vue"
18
- },
19
- {
20
- name: "FileManager",
21
- path: "/FileManager",
22
- file: "runtime/pages/FileManager/index.vue"
23
- },
24
- {
25
- name: "MailLog",
26
- path: "/MailLog",
27
- file: "runtime/pages/MailLog/index.vue"
28
- },
29
- {
30
- name: "Permission",
31
- path: "/Permission",
32
- file: "runtime/pages/Permission/index.vue"
33
- },
34
- {
35
- name: "Role",
36
- path: "/Role",
37
- file: "runtime/pages/Role/index.vue"
38
- },
39
- {
40
- name: "System",
41
- path: "/System",
42
- file: "runtime/pages/System/index.vue"
43
- },
44
- {
45
- name: "SystemValue",
46
- path: "/SystemValue",
47
- file: "runtime/pages/SystemValue/index.vue"
48
- },
49
- {
50
- name: "Translate",
51
- path: "/Translate",
52
- file: "runtime/pages/Translate/index.vue"
53
- },
54
- {
55
- name: "User",
56
- path: "/User",
57
- file: "runtime/pages/User/index.vue"
58
- },
59
- {
60
- name: "UserLog",
61
- path: "/UserLog",
62
- file: "runtime/pages/UserLog/index.vue"
63
- },
64
- {
65
- name: "Permission-add",
66
- path: "/Permission/add",
67
- file: "runtime/pages/Permission/add.vue"
68
- },
69
- {
70
- name: "Permission-all",
71
- path: "/Permission/all",
72
- file: "runtime/pages/Permission/all.vue"
73
- },
74
- {
75
- name: "Permission-export",
76
- path: "/Permission/export",
77
- file: "runtime/pages/Permission/export.vue"
78
- },
79
- {
80
- name: "Role-add",
81
- path: "/Role/add",
82
- file: "runtime/pages/Role/add.vue"
83
- },
84
- {
85
- name: "Role-add2",
86
- path: "/Role/add2",
87
- file: "runtime/pages/Role/add2.vue"
88
- },
89
- {
90
- name: "System-fs",
91
- path: "/System/fs",
92
- file: "runtime/pages/System/fs.vue"
93
- },
94
- {
95
- name: "System-mailtest",
96
- path: "/System/mailtest",
97
- file: "runtime/pages/System/mailtest.vue"
98
- },
99
- {
100
- name: "System-package",
101
- path: "/System/package",
102
- file: "runtime/pages/System/package.vue"
103
- },
104
- {
105
- name: "System-phpinfo",
106
- path: "/System/phpinfo",
107
- file: "runtime/pages/System/phpinfo.vue"
108
- },
109
- {
110
- name: "System-setting",
111
- path: "/System/setting",
112
- file: "runtime/pages/System/setting.vue"
113
- },
114
- {
115
- name: "System-test",
116
- path: "/System/test",
117
- file: "runtime/pages/System/test.vue"
118
- },
119
- {
120
- name: "System-view_as",
121
- path: "/System/view_as",
122
- file: "runtime/pages/System/view_as.vue"
123
- },
124
- {
125
- name: "System-menu",
126
- path: "/System/menu",
127
- file: "runtime/pages/System/menu/index.vue"
128
- },
129
- {
130
- name: "SystemValue-add",
131
- path: "/SystemValue/add",
132
- file: "runtime/pages/SystemValue/add.vue"
133
- },
134
- {
135
- name: "System-database-check",
136
- path: "/System/database/check",
137
- file: "runtime/pages/System/database/check.vue"
138
- },
139
- {
140
- name: "System-database-migrate",
141
- path: "/System/database/migrate",
142
- file: "runtime/pages/System/database/migrate.vue"
143
- },
144
- {
145
- name: "System-database-process",
146
- path: "/System/database/process",
147
- file: "runtime/pages/System/database/process.vue"
148
- },
149
- {
150
- name: "System-database-restore",
151
- path: "/System/database/restore",
152
- file: "runtime/pages/System/database/restore.vue"
153
- },
154
- {
155
- name: "System-database-backup",
156
- path: "/System/database/backup",
157
- file: "runtime/pages/System/database/backup.vue"
158
- },
159
- {
160
- name: "System-database-table",
161
- path: "/System/database/table",
162
- file: "runtime/pages/System/database/table.vue"
163
- },
164
- {
165
- name: "System-database-event",
166
- path: "/System/database/event",
167
- file: "runtime/pages/System/database/event.vue"
168
- },
169
- {
170
- name: "EventLog-eventlog_id-view",
171
- path: "/EventLog/:eventlog_id/view",
172
- file: "runtime/pages/EventLog/_eventlog_id/view.vue"
173
- },
174
- {
175
- name: "Role-name-update-child",
176
- path: "/Role/:name/update-child",
177
- file: "runtime/pages/Role/_name/update-child.vue"
178
- },
179
- {
180
- name: "SystemValue-systemvalue_id-edit",
181
- path: "/SystemValue/:systemvalue_id/edit",
182
- file: "runtime/pages/SystemValue/_systemvalue_id/edit.vue"
183
- },
184
- {
185
- name: "User-user_id-change-password",
186
- path: "/User/:user_id/change-password",
187
- file: "runtime/pages/User/_user_id/change-password.vue"
188
- },
189
- {
190
- name: "User-user_id-edit",
191
- path: "/User/:user_id/edit",
192
- file: "runtime/pages/User/_user_id/edit.vue"
193
- },
194
- {
195
- name: "User-user_id-update-role",
196
- path: "/User/:user_id/update-role",
197
- file: "runtime/pages/User/_user_id/update-role.vue"
198
- },
199
- {
200
- name: "User",
201
- path: "/User",
202
- file: "runtime/pages/User/index.vue"
203
- },
204
- {
205
- name: "User-add",
206
- path: "/User/add",
207
- file: "runtime/pages/User/add.vue"
208
- },
209
- {
210
- name: "User-user_id-view",
211
- path: "/User/:user_id/view",
212
- file: "runtime/pages/User/_user_id/view.vue"
213
- },
214
- {
215
- name: "User-profile",
216
- path: "/User/profile",
217
- file: "runtime/pages/User/profile.vue"
218
- },
219
- {
220
- name: "User-createAccessToken",
221
- path: "/User/createAccessToken",
222
- file: "runtime/pages/User/createAccessToken.vue"
223
- },
224
- {
225
- path: "/User/setting",
226
- file: "runtime/pages/User/setting.vue",
227
- children: [
228
- {
229
- name: "User-setting",
230
- path: "",
231
- file: "runtime/pages/User/setting/index.vue"
232
- },
233
- {
234
- name: "User-setting-bio-auth",
235
- path: "bio-auth",
236
- file: "runtime/pages/User/setting/bio-auth.vue"
237
- },
238
- {
239
- name: "User-setting-information",
240
- path: "information",
241
- file: "runtime/pages/User/setting/information.vue"
242
- },
243
- {
244
- name: "User-setting-favorite",
245
- path: "favorite",
246
- file: "runtime/pages/User/setting/favorite.vue"
247
- },
248
- {
249
- name: "User-setting-menu",
250
- path: "menu",
251
- file: "runtime/pages/User/setting/menu.vue"
252
- },
253
- {
254
- name: "User-setting-open_id",
255
- path: "open_id",
256
- file: "runtime/pages/User/setting/open_id.vue"
257
- },
258
- {
259
- name: "User-setting-password",
260
- path: "password",
261
- file: "runtime/pages/User/setting/password.vue"
262
- },
263
- {
264
- name: "User-setting-style",
265
- path: "style",
266
- file: "runtime/pages/User/setting/style.vue"
267
- },
268
- {
269
- name: "User-setting-two-factor-auth",
270
- path: "two-factor-auth",
271
- file: "runtime/pages/User/setting/two-factor-auth.vue"
272
- },
273
- {
274
- name: "User-setting-sessions",
275
- path: "sessions",
276
- file: "runtime/pages/User/setting/sessions.vue"
5
+ function buildPath(dirParts, fileName) {
6
+ const parts = [...dirParts];
7
+ if (fileName !== "index") {
8
+ parts.push(fileName);
9
+ }
10
+ return "/" + parts.map((p) => {
11
+ if (p.startsWith("[") && p.endsWith("]")) {
12
+ return ":" + p.slice(1, -1);
13
+ }
14
+ return p;
15
+ }).join("/");
16
+ }
17
+ function buildName(dirParts, fileName) {
18
+ const parts = [...dirParts];
19
+ if (fileName !== "index") {
20
+ parts.push(fileName);
21
+ }
22
+ return parts.map((p) => {
23
+ if (p.startsWith("[") && p.endsWith("]")) {
24
+ return p.slice(1, -1);
25
+ }
26
+ return p;
27
+ }).join("-");
28
+ }
29
+ function scanRoutes(pagesDir, dir) {
30
+ const entries = readdirSync(dir, { withFileTypes: true });
31
+ const files = entries.filter((e) => e.isFile() && e.name.endsWith(".vue")).sort((a, b) => {
32
+ if (a.name === "index.vue") return -1;
33
+ if (b.name === "index.vue") return 1;
34
+ return a.name.localeCompare(b.name);
35
+ });
36
+ const dirs = entries.filter((e) => e.isDirectory());
37
+ const result = [];
38
+ const handledDirs = /* @__PURE__ */ new Set();
39
+ for (const file of files) {
40
+ const baseName = basename(file.name, ".vue");
41
+ const siblingDir = dirs.find((d) => d.name === baseName);
42
+ if (siblingDir) {
43
+ handledDirs.add(baseName);
44
+ const filePath = join(dir, file.name);
45
+ const relPath = relative(pagesDir, filePath);
46
+ const dirParts = dirname(relPath).split("/").filter((p) => p && p !== ".");
47
+ const parentPath = buildPath(dirParts, baseName);
48
+ const children = scanRoutes(pagesDir, join(dir, baseName));
49
+ for (const child of children) {
50
+ child.path = child.path.replace(parentPath, "");
51
+ if (child.path.startsWith("/")) {
52
+ child.path = child.path.slice(1);
53
+ }
277
54
  }
278
- ]
55
+ result.push({
56
+ path: parentPath,
57
+ file: relPath,
58
+ children
59
+ });
60
+ }
61
+ }
62
+ for (const file of files) {
63
+ const baseName = basename(file.name, ".vue");
64
+ const siblingDir = dirs.find((d) => d.name === baseName);
65
+ if (!siblingDir) {
66
+ const filePath = join(dir, file.name);
67
+ const relPath = relative(pagesDir, filePath);
68
+ const dirParts = dirname(relPath).split("/").filter((p) => p && p !== ".");
69
+ result.push({
70
+ name: buildName(dirParts, baseName),
71
+ path: buildPath(dirParts, baseName),
72
+ file: relPath
73
+ });
74
+ }
279
75
  }
280
- ];
76
+ for (const d of dirs) {
77
+ if (!handledDirs.has(d.name)) {
78
+ result.push(...scanRoutes(pagesDir, join(dir, d.name)));
79
+ }
80
+ }
81
+ return result;
82
+ }
83
+ function generateRoutes(pagesDir) {
84
+ return scanRoutes(pagesDir, pagesDir);
85
+ }
86
+ function resolveRouteFiles(routes, resolve) {
87
+ return routes.map((route) => {
88
+ const resolved = {
89
+ ...route,
90
+ file: resolve(`runtime/pages/${route.file}`)
91
+ };
92
+ if (route.children) {
93
+ resolved.children = resolveRouteFiles(route.children, resolve);
94
+ }
95
+ return resolved;
96
+ });
97
+ }
281
98
 
282
99
  const module$1 = defineNuxtModule({
283
100
  meta: {
@@ -349,26 +166,26 @@ const module$1 = defineNuxtModule({
349
166
  }
350
167
  };
351
168
  nuxt.options.runtimeConfig = runtimeConfig;
169
+ const pagesDir = resolver.resolve("./runtime/pages");
170
+ const generatedRoutes = resolveRouteFiles(generateRoutes(pagesDir), resolver.resolve);
352
171
  extendPages((pages) => {
353
- for (const route of routes) {
172
+ for (const route of generatedRoutes) {
354
173
  if (route.children) {
355
174
  pages.unshift({
356
175
  path: route.path,
357
- file: resolver.resolve(route.file),
358
- children: route.children.map((child) => {
359
- return {
360
- name: child.name,
361
- path: child.path,
362
- file: resolver.resolve(child.file)
363
- };
364
- })
176
+ file: route.file,
177
+ children: route.children.map((child) => ({
178
+ name: child.name,
179
+ path: child.path,
180
+ file: child.file
181
+ }))
365
182
  });
366
183
  continue;
367
184
  }
368
185
  pages.unshift({
369
186
  name: route.name,
370
187
  path: route.path,
371
- file: resolver.resolve(route.file)
188
+ file: route.file
372
189
  });
373
190
  }
374
191
  });
@@ -419,7 +236,9 @@ export default defineNuxtPlugin(() => {})`;
419
236
  import { query as q } from '@hostlink/light'
420
237
  export default defineNuxtPlugin(() => {
421
238
  addRouteMiddleware("auth", async (to) => {
422
- if (to.path === "/") return;
239
+ const normalize = (p) => (p || "").replace(/[/]+$/, "");
240
+ const publicPaths = ["/", "/page_not_found", "/login"].map(normalize);
241
+ if (publicPaths.includes(normalize(to.path))) return;
423
242
  const { my } = await q({
424
243
  my: { allowedPath: { __args: { path: to.path } } }
425
244
  });
@@ -1,12 +1,12 @@
1
- declare var __VLS_38: {}, __VLS_240: {}, __VLS_351: {}, __VLS_358: {};
1
+ declare var __VLS_38: {}, __VLS_245: {}, __VLS_356: {}, __VLS_363: {};
2
2
  type __VLS_Slots = {} & {
3
3
  header?: (props: typeof __VLS_38) => any;
4
4
  } & {
5
- 'user-menu'?: (props: typeof __VLS_240) => any;
5
+ 'user-menu'?: (props: typeof __VLS_245) => any;
6
6
  } & {
7
- 'page-top'?: (props: typeof __VLS_351) => any;
7
+ 'page-top'?: (props: typeof __VLS_356) => any;
8
8
  } & {
9
- 'page-bottom'?: (props: typeof __VLS_358) => any;
9
+ 'page-bottom'?: (props: typeof __VLS_363) => any;
10
10
  };
11
11
  declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
12
12
  logout: (...args: any[]) => void;
@@ -28,6 +28,7 @@ const tt = await q({
28
28
  copyrightName: true,
29
29
  copyrightYear: true,
30
30
  hasFavorite: true,
31
+ notificationBellEnabled: true,
31
32
  i18nMessages: {
32
33
  name: true,
33
34
  value: true
@@ -288,6 +289,8 @@ const onLogout = async () => {
288
289
  </q-menu>
289
290
  </q-btn>
290
291
 
292
+ <l-notification-bell class="q-mr-xs" v-if="app.notificationBellEnabled !== false" />
293
+
291
294
  <div class="q-mx-sm" v-if="$q.screen.gt.xs">
292
295
  <div class="text-bold text-right">
293
296
  {{ my.first_name }} {{ my.last_name }}
@@ -1,12 +1,12 @@
1
- declare var __VLS_38: {}, __VLS_240: {}, __VLS_351: {}, __VLS_358: {};
1
+ declare var __VLS_38: {}, __VLS_245: {}, __VLS_356: {}, __VLS_363: {};
2
2
  type __VLS_Slots = {} & {
3
3
  header?: (props: typeof __VLS_38) => any;
4
4
  } & {
5
- 'user-menu'?: (props: typeof __VLS_240) => any;
5
+ 'user-menu'?: (props: typeof __VLS_245) => any;
6
6
  } & {
7
- 'page-top'?: (props: typeof __VLS_351) => any;
7
+ 'page-top'?: (props: typeof __VLS_356) => any;
8
8
  } & {
9
- 'page-bottom'?: (props: typeof __VLS_358) => any;
9
+ 'page-bottom'?: (props: typeof __VLS_363) => any;
10
10
  };
11
11
  declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
12
12
  logout: (...args: any[]) => void;
@@ -0,0 +1,187 @@
1
+ <script setup>
2
+ import { ref, computed, onMounted, onUnmounted } from "vue";
3
+ import { useQuasar } from "quasar";
4
+ import { useLight, q, m, navigateTo } from "#imports";
5
+ const $q = useQuasar();
6
+ const light = useLight();
7
+ const notifications = ref([]);
8
+ const unreadCount = ref(0);
9
+ const loading = ref(false);
10
+ const fetchUnreadCount = async () => {
11
+ try {
12
+ const data = await q({
13
+ my: {
14
+ unreadNotificationCount: true
15
+ }
16
+ });
17
+ unreadCount.value = data.my.unreadNotificationCount || 0;
18
+ } catch {
19
+ }
20
+ };
21
+ const fetchNotifications = async () => {
22
+ loading.value = true;
23
+ try {
24
+ const data = await q({
25
+ app: {
26
+ listNotification: {
27
+ data: {
28
+ __args: {
29
+ limit: 10,
30
+ offset: 0
31
+ },
32
+ notification_id: true,
33
+ type: true,
34
+ title: true,
35
+ message: true,
36
+ link: true,
37
+ is_read: true,
38
+ created_time: true
39
+ },
40
+ meta: {
41
+ total: true,
42
+ key: true,
43
+ name: true
44
+ }
45
+ }
46
+ }
47
+ });
48
+ notifications.value = data.app.listNotification.data || [];
49
+ } catch (e) {
50
+ $q.notify({ message: e.message, color: "negative" });
51
+ } finally {
52
+ loading.value = false;
53
+ }
54
+ };
55
+ const markRead = async (notification) => {
56
+ if (notification.is_read) return;
57
+ try {
58
+ await m("markNotificationRead", { id: notification.notification_id });
59
+ notification.is_read = 1;
60
+ unreadCount.value = Math.max(0, unreadCount.value - 1);
61
+ } catch (e) {
62
+ $q.notify({ message: e.message, color: "negative" });
63
+ }
64
+ };
65
+ const markAllRead = async () => {
66
+ try {
67
+ await m("markAllNotificationsRead");
68
+ notifications.value.forEach((n) => n.is_read = 1);
69
+ unreadCount.value = 0;
70
+ } catch (e) {
71
+ $q.notify({ message: e.message, color: "negative" });
72
+ }
73
+ };
74
+ const goToInbox = () => {
75
+ navigateTo("/Notification");
76
+ };
77
+ const onClickNotification = (notification) => {
78
+ markRead(notification);
79
+ if (notification.link) {
80
+ navigateTo(notification.link);
81
+ }
82
+ };
83
+ const iconForType = (type) => {
84
+ switch (type) {
85
+ case "success":
86
+ return "sym_o_check_circle";
87
+ case "warning":
88
+ return "sym_o_warning";
89
+ case "error":
90
+ return "sym_o_error";
91
+ default:
92
+ return "sym_o_info";
93
+ }
94
+ };
95
+ const colorForType = (type) => {
96
+ switch (type) {
97
+ case "success":
98
+ return "positive";
99
+ case "warning":
100
+ return "warning";
101
+ case "error":
102
+ return "negative";
103
+ default:
104
+ return "info";
105
+ }
106
+ };
107
+ let pollInterval;
108
+ onMounted(() => {
109
+ fetchUnreadCount();
110
+ pollInterval = setInterval(fetchUnreadCount, 6e4);
111
+ });
112
+ onUnmounted(() => {
113
+ if (pollInterval) clearInterval(pollInterval);
114
+ });
115
+ const openDropdown = () => {
116
+ };
117
+ const unreadNotifications = computed(() => notifications.value.filter((n) => !n.is_read));
118
+ const readNotifications = computed(() => notifications.value.filter((n) => n.is_read));
119
+ </script>
120
+
121
+ <template>
122
+ <q-btn dense flat round icon="sym_o_notifications" class="q-mr-xs">
123
+ <q-badge v-if="unreadCount > 0" color="negative" floating rounded>{{ unreadCount > 99 ? '99+' : unreadCount }}</q-badge>
124
+ <q-menu :offset="[0, 8]" max-width="360px" max-height="480px" auto-close @show="fetchNotifications">
125
+ <q-card flat bordered>
126
+ <q-card-section class="row items-center justify-between q-py-sm">
127
+ <div class="text-subtitle2">{{ $t('Notifications') }}</div>
128
+ <q-btn v-if="unreadCount > 0" flat dense size="sm" color="primary" @click="markAllRead">
129
+ {{ $t('Mark all read') }}
130
+ </q-btn>
131
+ </q-card-section>
132
+ <q-separator />
133
+
134
+ <q-card-section v-if="loading" class="text-center q-py-md">
135
+ <q-spinner color="primary" size="2em" />
136
+ </q-card-section>
137
+
138
+ <q-list v-else-if="notifications.length" padding dense separator>
139
+ <template v-if="unreadNotifications.length">
140
+ <q-item-label header class="text-caption text-uppercase text-grey">{{ $t('Unread') }}</q-item-label>
141
+ <q-item v-for="n in unreadNotifications" :key="n.notification_id" clickable v-ripple
142
+ class="bg-blue-1" @click="onClickNotification(n)">
143
+ <q-item-section avatar top>
144
+ <q-icon :name="iconForType(n.type)" :color="colorForType(n.type)" size="24px" />
145
+ </q-item-section>
146
+ <q-item-section>
147
+ <q-item-label class="text-weight-bold">{{ n.title }}</q-item-label>
148
+ <q-item-label caption lines="2">{{ n.message }}</q-item-label>
149
+ <q-item-label caption class="text-grey">{{ n.created_time }}</q-item-label>
150
+ </q-item-section>
151
+ <q-item-section side>
152
+ <q-btn flat round dense icon="sym_o_done" size="sm" color="primary"
153
+ @click.stop="markRead(n)">
154
+ <q-tooltip>{{ $t('Mark as read') }}</q-tooltip>
155
+ </q-btn>
156
+ </q-item-section>
157
+ </q-item>
158
+ </template>
159
+
160
+ <template v-if="readNotifications.length">
161
+ <q-item-label header class="text-caption text-uppercase text-grey">{{ $t('Earlier') }}</q-item-label>
162
+ <q-item v-for="n in readNotifications" :key="n.notification_id" clickable v-ripple
163
+ @click="onClickNotification(n)">
164
+ <q-item-section avatar top>
165
+ <q-icon :name="iconForType(n.type)" color="grey" size="24px" />
166
+ </q-item-section>
167
+ <q-item-section>
168
+ <q-item-label>{{ n.title }}</q-item-label>
169
+ <q-item-label caption lines="2">{{ n.message }}</q-item-label>
170
+ <q-item-label caption class="text-grey">{{ n.created_time }}</q-item-label>
171
+ </q-item-section>
172
+ </q-item>
173
+ </template>
174
+ </q-list>
175
+
176
+ <q-card-section v-else class="text-center text-grey q-py-md">
177
+ {{ $t('No notifications') }}
178
+ </q-card-section>
179
+
180
+ <q-separator />
181
+ <q-card-actions align="center">
182
+ <q-btn flat color="primary" size="sm" @click="goToInbox">{{ $t('View all notifications') }}</q-btn>
183
+ </q-card-actions>
184
+ </q-card>
185
+ </q-menu>
186
+ </q-btn>
187
+ </template>
@@ -1,4 +1,5 @@
1
1
  <script setup>
2
+ import { q } from "#imports";
2
3
  const emits = defineEmits(["submit"]);
3
4
  defineProps({
4
5
  mail_driver: { type: String, required: true, default: "mail" },
@@ -12,12 +13,6 @@ defineProps({
12
13
  mail_reply_to: { type: String, required: true },
13
14
  mail_reply_to_name: { type: String, required: true }
14
15
  });
15
- const onLoginGmail = () => {
16
- let state = encodeURIComponent(window.self.location.origin + window.self.location.pathname + "?mail_driver=gmail");
17
- let scope = encodeURIComponent("https://mail.google.com/");
18
- let url = "https://accounts.google.com/o/oauth2/v2/auth?scope=" + scope + "&access_type=offline&state=" + state + "&response_type=code&redirect_uri=https%3A%2F%2Fraymond4.hostlink.com.hk%2Flight%2Fgmail_notification%2Fredirect.php&client_id=790028313082-8qqnoqvkqtqssufto11k6qe6pnievcpv.apps.googleusercontent.com&prompt=consent";
19
- window.open(url, "_blank");
20
- };
21
16
  </script>
22
17
 
23
18
  <template>
@@ -35,7 +30,19 @@ const onLoginGmail = () => {
35
30
  ]" validation="required"></FormKit>
36
31
 
37
32
  <template v-if="value.mail_driver === 'gmail'">
38
- <l-btn label="Login" @click="onLoginGmail" icon="sym_o_login" />
33
+ <div class="col-12">
34
+ <q-banner class="bg-info text-white" rounded>
35
+ <div class="text-weight-bold q-mb-xs">How to create an App Password</div>
36
+ <ol class="q-pl-md q-my-none">
37
+ <li>Enable 2-Step Verification in your Google Account Security settings.</li>
38
+ <li>Go to the <a href="https://myaccount.google.com/apppasswords" target="_blank" class="text-white" style="text-decoration: underline;">Google App Passwords page</a>.</li>
39
+ <li>Select app and device, or choose Other (Custom name), then click Generate.</li>
40
+ <li>Copy the 16-character password and paste it below.</li>
41
+ </ol>
42
+ </q-banner>
43
+ </div>
44
+ <FormKit type="l-input" label="Gmail Address" name="mail_username" validation="required|email"></FormKit>
45
+ <FormKit type="l-input" label="App Password" name="mail_password" validation="required" :attrs="{ type: 'password' }"></FormKit>
39
46
  </template>
40
47
 
41
48
  <template v-else>
@@ -50,7 +57,7 @@ const onLoginGmail = () => {
50
57
  <FormKit type="l-input" label="SMTP Host" name="mail_host"></FormKit>
51
58
  <FormKit type="l-input" label="SMTP Port" name="mail_port"></FormKit>
52
59
  <FormKit type="l-input" label="SMTP Username" name="mail_username"></FormKit>
53
- <FormKit type="l-input" label="SMTP Password" name="mail_password"></FormKit>
60
+ <FormKit type="l-input" label="SMTP Password" name="mail_password" :attrs="{ type: 'password' }"></FormKit>
54
61
 
55
62
  <FormKit type="l-select" label="SMTP Encryption" name="mail_encryption" :options="[
56
63
  { label: 'None', value: '' },
@@ -1,5 +1,6 @@
1
1
  type __VLS_Props = {
2
2
  file_manager: string;
3
+ notification_bell_enabled: string;
3
4
  };
4
5
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
5
6
  submit: (...args: any[]) => void;
@@ -1,7 +1,8 @@
1
1
  <script setup>
2
2
  const emits = defineEmits(["submit"]);
3
3
  defineProps({
4
- file_manager: { type: String, required: true }
4
+ file_manager: { type: String, required: true },
5
+ notification_bell_enabled: { type: String, required: true }
5
6
  });
6
7
  </script>
7
8
 
@@ -10,6 +11,9 @@ defineProps({
10
11
  <l-field :label="$t('File Manager')" stack-label hide-bottom-space class="col-6">
11
12
  <form-kit type="l-checkbox" label="Show" name="file_manager" true-value="1" false-value="0" />
12
13
  </l-field>
13
-
14
+
15
+ <l-field :label="$t('Notification Bell')" stack-label hide-bottom-space class="col-6">
16
+ <form-kit type="l-checkbox" label="Show" name="notification_bell_enabled" true-value="1" false-value="0" />
17
+ </l-field>
14
18
  </form-kit>
15
19
  </template>
@@ -1,5 +1,6 @@
1
1
  type __VLS_Props = {
2
2
  file_manager: string;
3
+ notification_bell_enabled: string;
3
4
  };
4
5
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
5
6
  submit: (...args: any[]) => void;
@@ -79,17 +79,23 @@ export interface LTableRequest {
79
79
  }) => void;
80
80
  }
81
81
  declare function requestServerInteraction(): void;
82
- declare var __VLS_114: any, __VLS_117: string, __VLS_118: any, __VLS_149: any, __VLS_152: any, __VLS_322: string, __VLS_323: any;
82
+ declare var __VLS_24: {
83
+ props: any;
84
+ }, __VLS_55: any, __VLS_119: any, __VLS_122: string, __VLS_123: any, __VLS_154: any, __VLS_157: any, __VLS_327: string, __VLS_328: any;
83
85
  type __VLS_Slots = {} & {
84
- [K in NonNullable<typeof __VLS_117>]?: (props: typeof __VLS_118) => any;
86
+ [K in NonNullable<typeof __VLS_122>]?: (props: typeof __VLS_123) => any;
85
87
  } & {
86
- [K in NonNullable<typeof __VLS_322>]?: (props: typeof __VLS_323) => any;
88
+ [K in NonNullable<typeof __VLS_327>]?: (props: typeof __VLS_328) => any;
87
89
  } & {
88
- actions?: (props: typeof __VLS_114) => any;
90
+ 'header-selection'?: (props: typeof __VLS_24) => any;
89
91
  } & {
90
- 'row-expand'?: (props: typeof __VLS_149) => any;
92
+ 'top-selection'?: (props: typeof __VLS_55) => any;
91
93
  } & {
92
- 'top-right'?: (props: typeof __VLS_152) => any;
94
+ actions?: (props: typeof __VLS_119) => any;
95
+ } & {
96
+ 'row-expand'?: (props: typeof __VLS_154) => any;
97
+ } & {
98
+ 'top-right'?: (props: typeof __VLS_157) => any;
93
99
  };
94
100
  declare const __VLS_base: import("vue").DefineComponent<LTableProps, {
95
101
  requestServerInteraction: typeof requestServerInteraction;
@@ -562,7 +562,9 @@ const hasFilters = computed(() => {
562
562
  <template #header="props">
563
563
  <q-tr :props="props">
564
564
  <q-th v-if="selection != 'none'" auto-width>
565
- <q-checkbox v-model="props.selected" :dense="table?.dense" />
565
+ <slot name="header-selection" :props="props">
566
+ <q-checkbox v-model="props.selected" :dense="table?.dense" />
567
+ </slot>
566
568
  </q-th>
567
569
  <q-th v-if="hasRowExpand" auto-width></q-th>
568
570
  <q-th v-if="hasActions" auto-width></q-th>
@@ -578,6 +580,10 @@ const hasFilters = computed(() => {
578
580
  </div>
579
581
  </template>
580
582
 
583
+ <template #top-selection="props" v-if="ss.indexOf('top-selection') >= 0">
584
+ <slot name="top-selection" v-bind="props"></slot>
585
+ </template>
586
+
581
587
  <template #top-left v-if="addComponent">
582
588
  <q-btn icon="sym_o_add" :label="$t('Add')" color="primary" @click="onAdd" outline />
583
589
  </template>
@@ -79,17 +79,23 @@ export interface LTableRequest {
79
79
  }) => void;
80
80
  }
81
81
  declare function requestServerInteraction(): void;
82
- declare var __VLS_114: any, __VLS_117: string, __VLS_118: any, __VLS_149: any, __VLS_152: any, __VLS_322: string, __VLS_323: any;
82
+ declare var __VLS_24: {
83
+ props: any;
84
+ }, __VLS_55: any, __VLS_119: any, __VLS_122: string, __VLS_123: any, __VLS_154: any, __VLS_157: any, __VLS_327: string, __VLS_328: any;
83
85
  type __VLS_Slots = {} & {
84
- [K in NonNullable<typeof __VLS_117>]?: (props: typeof __VLS_118) => any;
86
+ [K in NonNullable<typeof __VLS_122>]?: (props: typeof __VLS_123) => any;
85
87
  } & {
86
- [K in NonNullable<typeof __VLS_322>]?: (props: typeof __VLS_323) => any;
88
+ [K in NonNullable<typeof __VLS_327>]?: (props: typeof __VLS_328) => any;
87
89
  } & {
88
- actions?: (props: typeof __VLS_114) => any;
90
+ 'header-selection'?: (props: typeof __VLS_24) => any;
89
91
  } & {
90
- 'row-expand'?: (props: typeof __VLS_149) => any;
92
+ 'top-selection'?: (props: typeof __VLS_55) => any;
91
93
  } & {
92
- 'top-right'?: (props: typeof __VLS_152) => any;
94
+ actions?: (props: typeof __VLS_119) => any;
95
+ } & {
96
+ 'row-expand'?: (props: typeof __VLS_154) => any;
97
+ } & {
98
+ 'top-right'?: (props: typeof __VLS_157) => any;
93
99
  };
94
100
  declare const __VLS_base: import("vue").DefineComponent<LTableProps, {
95
101
  requestServerInteraction: typeof requestServerInteraction;
@@ -0,0 +1,2 @@
1
+ declare const _default: () => void;
2
+ export default _default;
@@ -0,0 +1,52 @@
1
+ import { defineLightModel } from "#imports";
2
+ export default defineLightModel({
3
+ name: "Notification",
4
+ dataPath: "app.listNotification",
5
+ fields: {
6
+ notification_id: {
7
+ label: "ID",
8
+ sortable: true
9
+ },
10
+ type: {
11
+ label: "Type",
12
+ sortable: true,
13
+ searchable: true,
14
+ searchType: "select",
15
+ searchOptions: [
16
+ { label: "Info", value: "info" },
17
+ { label: "Success", value: "success" },
18
+ { label: "Warning", value: "warning" },
19
+ { label: "Error", value: "error" }
20
+ ],
21
+ format: (value) => {
22
+ const map = { info: "Info", success: "Success", warning: "Warning", error: "Error" };
23
+ return map[value] || value;
24
+ }
25
+ },
26
+ title: {
27
+ label: "Title",
28
+ sortable: true,
29
+ searchable: true
30
+ },
31
+ message: {
32
+ label: "Message",
33
+ searchable: true
34
+ },
35
+ link: {
36
+ label: "Link"
37
+ },
38
+ is_read: {
39
+ label: "Read",
40
+ sortable: true,
41
+ searchable: true,
42
+ searchType: "boolean",
43
+ format: (value) => value ? "Yes" : "No"
44
+ },
45
+ created_time: {
46
+ label: "Created Time",
47
+ sortable: true,
48
+ searchable: true,
49
+ searchType: "date"
50
+ }
51
+ }
52
+ });
@@ -5,7 +5,9 @@ export default defineLightModel({
5
5
  fields: {
6
6
  name: {
7
7
  label: "Name",
8
- sortable: true
8
+ sortable: true,
9
+ searchable: true,
10
+ searchPlaceholder: "Search by name..."
9
11
  },
10
12
  value: {
11
13
  label: "Value",
@@ -0,0 +1,55 @@
1
+ <script setup>
2
+ import { ref } from "vue";
3
+ import { useQuasar } from "quasar";
4
+ import { model, m, notify } from "#imports";
5
+ const $q = useQuasar();
6
+ const tableRef = ref(null);
7
+ const selected = ref([]);
8
+ const columns = model("Notification").columns({
9
+ notification_id: true,
10
+ type: true,
11
+ title: true,
12
+ message: true,
13
+ link: true,
14
+ is_read: true,
15
+ created_time: true
16
+ });
17
+ const onBulkDelete = async () => {
18
+ if (selected.value.length === 0) return;
19
+ $q.dialog({
20
+ title: $q.lang.label?.delete || "Delete",
21
+ message: "Are you sure you want to delete " + selected.value.length + " selected notification(s)?",
22
+ cancel: true,
23
+ persistent: true,
24
+ color: "negative"
25
+ }).onOk(async () => {
26
+ try {
27
+ const ids = selected.value.map((n) => n.notification_id);
28
+ await m("deleteNotifications", { ids });
29
+ notify("Deleted " + ids.length + " notification(s)");
30
+ selected.value = [];
31
+ tableRef.value?.requestServerInteraction();
32
+ } catch (e) {
33
+ notify({ message: e.message, color: "negative" });
34
+ }
35
+ });
36
+ };
37
+ const onDelete = () => {
38
+ tableRef.value?.requestServerInteraction();
39
+ };
40
+ </script>
41
+
42
+ <template>
43
+ <l-page>
44
+ <l-table ref="tableRef" row-key="notification_id" :columns="columns"
45
+ selection="multiple" v-model:selected="selected"
46
+ @request-data="$event.loadObjects('Notification')"
47
+ :actions="['delete']"
48
+ @delete="onDelete">
49
+ <template #top-selection="{ rows }">
50
+ <q-btn flat dense color="negative" icon="sym_o_delete"
51
+ :label="'Delete (' + selected.length + ')'" @click="onBulkDelete" />
52
+ </template>
53
+ </l-table>
54
+ </l-page>
55
+ </template>
@@ -49,7 +49,7 @@ let columns = [
49
49
  const onCickView = async (id) => {
50
50
  try {
51
51
  if (await m("viewAs", { user_id: id })) {
52
- window.location.reload();
52
+ window.location.href = "/";
53
53
  }
54
54
  } catch (e) {
55
55
  light.dialog({
@@ -0,0 +1,3 @@
1
+ declare const _default: typeof __VLS_export;
2
+ export default _default;
3
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -0,0 +1,3 @@
1
+ declare const _default: typeof __VLS_export;
2
+ export default _default;
3
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -0,0 +1,3 @@
1
+ declare const _default: typeof __VLS_export;
2
+ export default _default;
3
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -0,0 +1,3 @@
1
+ declare const _default: typeof __VLS_export;
2
+ export default _default;
3
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -0,0 +1,3 @@
1
+ declare const _default: typeof __VLS_export;
2
+ export default _default;
3
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -0,0 +1,26 @@
1
+ <script setup>
2
+ import { useRoute, navigateTo } from "#imports";
3
+ const route = useRoute();
4
+ const attemptedPath = route.query.path || route.fullPath;
5
+ </script>
6
+
7
+ <template>
8
+ <l-page class="q-pa-md">
9
+ <q-banner class="bg-negative text-white">
10
+ <template v-slot:avatar>
11
+ <q-icon name="sym_o_error" />
12
+ </template>
13
+ <div class="text-h6">Page not found</div>
14
+ <div v-if="attemptedPath && attemptedPath !== '/'">
15
+ The requested page was not found or you do not have permission to access it:
16
+ <code>{{ attemptedPath }}</code>
17
+ </div>
18
+ <div v-else>
19
+ The requested page was not found or you do not have permission to access it.
20
+ </div>
21
+ </q-banner>
22
+ <div class="q-mt-md">
23
+ <q-btn color="primary" icon="sym_o_home" label="Home" to="/" />
24
+ </div>
25
+ </l-page>
26
+ </template>
@@ -0,0 +1,3 @@
1
+ declare const _default: typeof __VLS_export;
2
+ export default _default;
3
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hostlink/nuxt-light",
3
- "version": "1.67.8",
3
+ "version": "1.69.0",
4
4
  "description": "HostLink Nuxt Light Framework",
5
5
  "repository": {
6
6
  "type": "git",
@@ -41,7 +41,7 @@
41
41
  "@hostlink/light": "^3.2.7",
42
42
  "@nuxt/module-builder": "^1.0.1",
43
43
  "@quasar/extras": "^1.17.0",
44
- "@quasar/quasar-ui-qmarkdown": "^2.0.5",
44
+ "@quasar/quasar-ui-qmarkdown": "^3.0.0-rc.1",
45
45
  "@vueuse/core": "^14.0.0",
46
46
  "axios": "^1.12.2",
47
47
  "defu": "^6.1.4",
@@ -65,6 +65,7 @@
65
65
  "eslint": "^9.39.1",
66
66
  "nuxt": "^4.3.0",
67
67
  "typescript": "^5.9.2",
68
+ "vitest": "^3.2.6",
68
69
  "vue-tsc": "^3.1.5"
69
70
  }
70
71
  }