create-nexgen 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +26 -0
- package/src/index.js +108 -0
- package/template/.dockerignore +14 -0
- package/template/.env +58 -0
- package/template/.env.example +59 -0
- package/template/.prettierignore +5 -0
- package/template/.prettierrc +8 -0
- package/template/README.md +447 -0
- package/template/drizzle.config.ts +29 -0
- package/template/eslint.config.js +52 -0
- package/template/gitignore-stub +24 -0
- package/template/package.json +96 -0
- package/template/public/assets/AuthLayout-CbswhpjJ.js +1 -0
- package/template/public/assets/Button-_7aQ7gHL.js +1 -0
- package/template/public/assets/Input-CLNJXmKc.css +1 -0
- package/template/public/assets/Input-z8GI8Aqo.js +1 -0
- package/template/public/assets/InputPasswordToggle-BxlzVGp3.js +1 -0
- package/template/public/assets/InputPasswordToggle-C77FI9Eg.css +1 -0
- package/template/public/assets/Layout-DotR1sQC.js +1 -0
- package/template/public/assets/Refresh-BdqsPPBC.js +1 -0
- package/template/public/assets/admin-ui-CU34rLdN.js +1 -0
- package/template/public/assets/bootstrap-icons-BeopsB42.woff +0 -0
- package/template/public/assets/bootstrap-icons-mSm7cUeB.woff2 +0 -0
- package/template/public/assets/dashboard-CwybEyLc.js +1 -0
- package/template/public/assets/dashboard-Dc4d-Pi7.css +1 -0
- package/template/public/assets/forgetPassword-CKEJaXsq.js +1 -0
- package/template/public/assets/index-Bleyx5dm.js +64 -0
- package/template/public/assets/index-DUw8E6Yg.css +1 -0
- package/template/public/assets/login-DC7PTlQF.js +1 -0
- package/template/public/assets/realtime-test-BPQdrFym.css +1 -0
- package/template/public/assets/realtime-test-tQZ0rBEJ.js +1 -0
- package/template/public/assets/register-3O7Qs28C.js +1 -0
- package/template/public/assets/resetPassword-A5AzMWKs.js +1 -0
- package/template/public/assets/verifyEmail-DDBEQHOv.js +1 -0
- package/template/public/index.html +17 -0
- package/template/src/database/migrations/mysql/0000_init.sql +73 -0
- package/template/src/database/migrations/mysql/meta/0000_snapshot.json +484 -0
- package/template/src/database/migrations/mysql/meta/_journal.json +13 -0
- package/template/src/database/schema.ts +4 -0
- package/template/src/env.ts +107 -0
- package/template/src/framework/cache/cache.ts +81 -0
- package/template/src/framework/database/connection.ts +168 -0
- package/template/src/framework/database/optional-db-drivers.d.ts +9 -0
- package/template/src/framework/database/paginate.ts +200 -0
- package/template/src/framework/database/schema.ts +26 -0
- package/template/src/framework/database/seed.ts +33 -0
- package/template/src/framework/events/dispatcher.ts +57 -0
- package/template/src/framework/facade.ts +27 -0
- package/template/src/framework/http/app.ts +61 -0
- package/template/src/framework/http/cors.ts +19 -0
- package/template/src/framework/http/logger.ts +85 -0
- package/template/src/framework/http/openapi.ts +34 -0
- package/template/src/framework/http/ratelimiter.ts +13 -0
- package/template/src/framework/http/router.ts +76 -0
- package/template/src/framework/http/static.ts +33 -0
- package/template/src/framework/http/validation.ts +24 -0
- package/template/src/framework/kernel.ts +40 -0
- package/template/src/framework/maker-cli/src/index.mjs +51 -0
- package/template/src/framework/maker-cli/src/levels/level-1/env-db.mjs +57 -0
- package/template/src/framework/maker-cli/src/levels/level-1/file-ops.mjs +30 -0
- package/template/src/framework/maker-cli/src/levels/level-1/flags.mjs +16 -0
- package/template/src/framework/maker-cli/src/levels/level-1/help.mjs +24 -0
- package/template/src/framework/maker-cli/src/levels/level-1/naming.mjs +13 -0
- package/template/src/framework/maker-cli/src/levels/level-1/process.mjs +47 -0
- package/template/src/framework/maker-cli/src/levels/level-2/db/core.mjs +299 -0
- package/template/src/framework/maker-cli/src/levels/level-2/db/index.mjs +177 -0
- package/template/src/framework/maker-cli/src/levels/level-2/deploy/core.mjs +635 -0
- package/template/src/framework/maker-cli/src/levels/level-2/deploy/index.mjs +145 -0
- package/template/src/framework/maker-cli/src/levels/level-2/module/core.mjs +707 -0
- package/template/src/framework/maker-cli/src/levels/level-2/module/index.mjs +116 -0
- package/template/src/framework/maker-cli/src/levels/level-2/runtime/build-frontend.mjs +16 -0
- package/template/src/framework/maker-cli/src/levels/level-2/runtime/core.mjs +311 -0
- package/template/src/framework/maker-cli/src/levels/level-2/runtime/index.mjs +71 -0
- package/template/src/framework/maker-cli/stubs/controller/openapi.ts.stub +55 -0
- package/template/src/framework/maker-cli/stubs/controller/openapi.with-model.ts.stub +56 -0
- package/template/src/framework/maker-cli/stubs/controller/plain.ts.stub +57 -0
- package/template/src/framework/maker-cli/stubs/controller/schema.plain.ts.stub +13 -0
- package/template/src/framework/maker-cli/stubs/controller/schema.ts.stub +32 -0
- package/template/src/framework/maker-cli/stubs/deploy/Dockerfile.bun.stub +49 -0
- package/template/src/framework/maker-cli/stubs/deploy/Dockerfile.pnpm.stub +53 -0
- package/template/src/framework/maker-cli/stubs/deploy/Dockerfile.stub +49 -0
- package/template/src/framework/maker-cli/stubs/deploy/Dockerfile.yarn.stub +53 -0
- package/template/src/framework/maker-cli/stubs/deploy/README.stub +55 -0
- package/template/src/framework/maker-cli/stubs/deploy/compose/mysql.server.stub +29 -0
- package/template/src/framework/maker-cli/stubs/deploy/compose/postgres.server.stub +29 -0
- package/template/src/framework/maker-cli/stubs/deploy/compose/sqlite.stub +29 -0
- package/template/src/framework/maker-cli/stubs/deploy/env/mysql.server.stub +73 -0
- package/template/src/framework/maker-cli/stubs/deploy/env/postgres.server.stub +73 -0
- package/template/src/framework/maker-cli/stubs/deploy/env/sqlite.stub +72 -0
- package/template/src/framework/maker-cli/stubs/deploy/scripts/auto-migrate.sh.stub +15 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/README.stub +77 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/compose/noredis.stub +118 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/compose/redis.dev.stub +131 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/compose/redis.stub +129 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/env/local.example.stub +10 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/env/noredis.stub +24 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/env/redis.stub +24 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/nginx-vhost/README.stub +15 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/nginx-vhost/app.example.com.stub +12 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/pgadmin/servers.stub +13 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/redis/redis.conf.stub +6 -0
- package/template/src/framework/maker-cli/stubs/deploy/supervisor/noredis.stub +53 -0
- package/template/src/framework/maker-cli/stubs/deploy/supervisor/redis.stub +69 -0
- package/template/src/framework/maker-cli/stubs/deploy/workflow/local.json.stub +24 -0
- package/template/src/framework/maker-cli/stubs/deploy/workflow/remote.json.stub +20 -0
- package/template/src/framework/maker-cli/stubs/example/console.ts.stub +33 -0
- package/template/src/framework/maker-cli/stubs/example/controller.ts.stub +503 -0
- package/template/src/framework/maker-cli/stubs/example/job.ts.stub +74 -0
- package/template/src/framework/maker-cli/stubs/example/route.api.ts.stub +206 -0
- package/template/src/framework/maker-cli/stubs/example/schema.ts.stub +41 -0
- package/template/src/framework/maker-cli/stubs/job/name.ts.stub +24 -0
- package/template/src/framework/maker-cli/stubs/model/name.mysql.ts.stub +8 -0
- package/template/src/framework/maker-cli/stubs/model/name.postgresql.ts.stub +8 -0
- package/template/src/framework/maker-cli/stubs/model/name.sqlite.ts.stub +8 -0
- package/template/src/framework/maker-cli/stubs/notification/NotificationBell.vue.stub +218 -0
- package/template/src/framework/maker-cli/stubs/notification/controller.ts.stub +85 -0
- package/template/src/framework/maker-cli/stubs/notification/index.vue.stub +211 -0
- package/template/src/framework/maker-cli/stubs/notification/job.ts.stub +12 -0
- package/template/src/framework/maker-cli/stubs/notification/route.api.ts.stub +49 -0
- package/template/src/framework/maker-cli/stubs/notification/schema.ts.stub +25 -0
- package/template/src/framework/maker-cli/stubs/route/api.ts.stub +79 -0
- package/template/src/framework/maker-cli/stubs/route/plain.ts.stub +10 -0
- package/template/src/framework/maker-cli/stubs/schedule/name.ts.stub +35 -0
- package/template/src/framework/maker-cli/stubs/seeder/name.ts.stub +17 -0
- package/template/src/framework/modules/discover.ts +54 -0
- package/template/src/framework/modules/routes.ts +26 -0
- package/template/src/framework/notification/index.ts +109 -0
- package/template/src/framework/queue/clear.ts +20 -0
- package/template/src/framework/queue/queue.ts +213 -0
- package/template/src/framework/queue/ui.ts +104 -0
- package/template/src/framework/queue/worker.ts +33 -0
- package/template/src/framework/realtime/broadcast.ts +27 -0
- package/template/src/framework/realtime/index.ts +1 -0
- package/template/src/framework/realtime/socket-cookie.ts +65 -0
- package/template/src/framework/realtime/socket.ts +132 -0
- package/template/src/framework/realtime/types.ts +6 -0
- package/template/src/framework/realtime/ui.ts +16 -0
- package/template/src/framework/redis/client.ts +126 -0
- package/template/src/framework/scheduler/lock.ts +124 -0
- package/template/src/framework/scheduler/run.ts +26 -0
- package/template/src/framework/scheduler/scheduler.ts +82 -0
- package/template/src/framework/server.ts +147 -0
- package/template/src/framework/session/session.ts +116 -0
- package/template/src/framework/storage/storage.ts +743 -0
- package/template/src/framework/support/cookie.ts +78 -0
- package/template/src/framework/support/jwt.ts +45 -0
- package/template/src/framework/support/lifecycle.ts +35 -0
- package/template/src/framework/support/logger.ts +102 -0
- package/template/src/framework/support/mail.ts +43 -0
- package/template/src/framework/support/password.ts +23 -0
- package/template/src/framework/support/url.ts +25 -0
- package/template/src/middlewares/auth-middleware.ts +98 -0
- package/template/src/middlewares/role-middleware.ts +24 -0
- package/template/src/modules/auth/controllers/auth.controller.ts +445 -0
- package/template/src/modules/auth/controllers/auth.helpers.ts +110 -0
- package/template/src/modules/auth/controllers/auth.schema.ts +102 -0
- package/template/src/modules/auth/controllers/role.controller.ts +25 -0
- package/template/src/modules/auth/database/models/notifications.ts +22 -0
- package/template/src/modules/auth/database/models/role.ts +14 -0
- package/template/src/modules/auth/database/models/user.ts +46 -0
- package/template/src/modules/auth/database/seeders/role.ts +19 -0
- package/template/src/modules/auth/database/seeders/user.ts +33 -0
- package/template/src/modules/auth/jobs/forgetpass.ts +18 -0
- package/template/src/modules/auth/jobs/registeruser.ts +31 -0
- package/template/src/modules/auth/jobs/verifyemail.ts +18 -0
- package/template/src/modules/auth/routes/api.ts +151 -0
- package/template/src/modules/auth/routes/role.ts +39 -0
- package/template/src/modules/welcome/controllers/welcome.controller.ts +14 -0
- package/template/src/modules/welcome/controllers/welcome.schema.ts +6 -0
- package/template/src/modules/welcome/database/models/welcome.ts +6 -0
- package/template/src/modules/welcome/routes/api.ts +20 -0
- package/template/src/resources/index.html +16 -0
- package/template/src/resources/src/App.vue +5 -0
- package/template/src/resources/src/assets/css/styles.css +14934 -0
- package/template/src/resources/src/assets/css/styles.css.map +1 -0
- package/template/src/resources/src/assets/images/favicon/favicon.ico +0 -0
- package/template/src/resources/src/assets/images/favicon/favicon1.ico +0 -0
- package/template/src/resources/src/assets/images/logo-1.png +0 -0
- package/template/src/resources/src/assets/images/logo-dark-sm.png +0 -0
- package/template/src/resources/src/assets/images/logo-dark.png +0 -0
- package/template/src/resources/src/assets/images/logo-dark1.png +0 -0
- package/template/src/resources/src/assets/images/logo-sm.png +0 -0
- package/template/src/resources/src/assets/images/logo1.png +0 -0
- package/template/src/resources/src/assets/images/logo2.png +0 -0
- package/template/src/resources/src/assets/scss/custom.css +217 -0
- package/template/src/resources/src/assets/scss/custom.css.map +1 -0
- package/template/src/resources/src/assets/scss/custom.scss +1100 -0
- package/template/src/resources/src/components/Button.vue +35 -0
- package/template/src/resources/src/components/Checkbox.vue +29 -0
- package/template/src/resources/src/components/FloatButton.vue +36 -0
- package/template/src/resources/src/components/Href.vue +32 -0
- package/template/src/resources/src/components/Input.vue +227 -0
- package/template/src/resources/src/components/InputGroup.vue +153 -0
- package/template/src/resources/src/components/InputPasswordToggle.vue +226 -0
- package/template/src/resources/src/components/Modal.vue +102 -0
- package/template/src/resources/src/components/Pagebar.vue +28 -0
- package/template/src/resources/src/components/Refresh.vue +26 -0
- package/template/src/resources/src/components/Select.vue +390 -0
- package/template/src/resources/src/components/Spinner.vue +42 -0
- package/template/src/resources/src/components/Switch.vue +65 -0
- package/template/src/resources/src/components/TextArea.vue +121 -0
- package/template/src/resources/src/components/Toast.vue +56 -0
- package/template/src/resources/src/components/datatable/DataTableSkeleton.vue +99 -0
- package/template/src/resources/src/components/datatable/Pagination.vue +161 -0
- package/template/src/resources/src/components/datatable/SelectOpption.vue +54 -0
- package/template/src/resources/src/components/datatable/index.vue +237 -0
- package/template/src/resources/src/composables/useAuth.ts +52 -0
- package/template/src/resources/src/composables/useBrowserDetect.ts +5 -0
- package/template/src/resources/src/composables/useDialog.ts +5 -0
- package/template/src/resources/src/composables/useGum.ts +3 -0
- package/template/src/resources/src/composables/usePulse.ts +5 -0
- package/template/src/resources/src/env.d.ts +20 -0
- package/template/src/resources/src/helpers/nformatter.ts +10 -0
- package/template/src/resources/src/helpers/utils.ts +68 -0
- package/template/src/resources/src/layouts/AuthLayout.vue +20 -0
- package/template/src/resources/src/layouts/Layout/Footer.vue +23 -0
- package/template/src/resources/src/layouts/Layout/Header.vue +90 -0
- package/template/src/resources/src/layouts/Layout/Sidebar.vue +137 -0
- package/template/src/resources/src/layouts/Layout/index.vue +76 -0
- package/template/src/resources/src/main.ts +27 -0
- package/template/src/resources/src/pages/auth/forgetPassword.vue +76 -0
- package/template/src/resources/src/pages/auth/login.vue +93 -0
- package/template/src/resources/src/pages/auth/register.vue +130 -0
- package/template/src/resources/src/pages/auth/resetPassword.vue +119 -0
- package/template/src/resources/src/pages/auth/verifyEmail.vue +60 -0
- package/template/src/resources/src/pages/dashboard/index.vue +76 -0
- package/template/src/resources/src/plugins/axios.ts +33 -0
- package/template/src/resources/src/plugins/browserDetect.ts +55 -0
- package/template/src/resources/src/plugins/dialog.ts +167 -0
- package/template/src/resources/src/plugins/gum.ts +343 -0
- package/template/src/resources/src/plugins/pulse.ts +141 -0
- package/template/src/resources/src/plugins/routeProgress.ts +87 -0
- package/template/src/resources/src/router/index.ts +85 -0
- package/template/src/resources/src/stores/admin-ui.ts +148 -0
- package/template/src/resources/src/stores/auth.ts +151 -0
- package/template/src/resources/tsconfig.json +19 -0
- package/template/src/resources/vite.config.ts +43 -0
- package/template/src/storage/logs/app.log +20179 -0
- package/template/src/storage/logs/fatal.log +727 -0
- package/template/tsconfig.json +20 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
ref,
|
|
4
|
+
reactive,
|
|
5
|
+
onMounted,
|
|
6
|
+
nextTick,
|
|
7
|
+
computed,
|
|
8
|
+
onBeforeUnmount,
|
|
9
|
+
watch,
|
|
10
|
+
useAttrs
|
|
11
|
+
} from "vue";
|
|
12
|
+
import { debounce } from "lodash-es";
|
|
13
|
+
import axios from "axios";
|
|
14
|
+
import vSelect from "vue-select";
|
|
15
|
+
import "vue-select/dist/vue-select.css";
|
|
16
|
+
import { empty } from "../helpers/utils";
|
|
17
|
+
|
|
18
|
+
defineOptions({ name: "Select", inheritAttrs: false });
|
|
19
|
+
|
|
20
|
+
type AnyRecord = Record<string, unknown>;
|
|
21
|
+
type SelectValue = string | number | boolean | AnyRecord | null;
|
|
22
|
+
type OptionHook = ((value: unknown) => void | Promise<void>) | null;
|
|
23
|
+
|
|
24
|
+
interface FetchPack {
|
|
25
|
+
url?: string | null;
|
|
26
|
+
data?: string | null;
|
|
27
|
+
params?: AnyRecord;
|
|
28
|
+
mapFn?: (x: AnyRecord) => AnyRecord;
|
|
29
|
+
option?:
|
|
30
|
+
| ((value: unknown) => void | Promise<void>)
|
|
31
|
+
| Array<(value: unknown) => void | Promise<void>>;
|
|
32
|
+
reset?: boolean;
|
|
33
|
+
reload?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface SelectProps {
|
|
37
|
+
fetched?: (payload: {
|
|
38
|
+
search: string;
|
|
39
|
+
reset?: boolean;
|
|
40
|
+
reload?: boolean;
|
|
41
|
+
}) => Promise<FetchPack | null | undefined> | FetchPack | null | undefined;
|
|
42
|
+
must?: boolean;
|
|
43
|
+
err?: string | boolean;
|
|
44
|
+
hood?: string | boolean;
|
|
45
|
+
defaultValue?: SelectValue;
|
|
46
|
+
resetKey?: string | number | boolean | AnyRecord | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const $attrs = useAttrs();
|
|
50
|
+
const props = withDefaults(defineProps<SelectProps>(), {
|
|
51
|
+
defaultValue: null,
|
|
52
|
+
resetKey: null
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// vue3.4 way
|
|
56
|
+
const emit = defineEmits<{
|
|
57
|
+
(event: "fetched"): void;
|
|
58
|
+
(event: "clear"): void;
|
|
59
|
+
}>();
|
|
60
|
+
const model = defineModel<SelectValue | SelectValue[]>();
|
|
61
|
+
|
|
62
|
+
const skipNextSearch = ref(false); // suppress search right after clear
|
|
63
|
+
|
|
64
|
+
let observer = ref<IntersectionObserver | null>(null);
|
|
65
|
+
let load = ref<HTMLElement | null>(null);
|
|
66
|
+
|
|
67
|
+
// store callbacks coming from fetchedDropdown(pack)
|
|
68
|
+
const hooks = ref<{ option: OptionHook | OptionHook[]; }>({ option: null }); // function | function[] | null
|
|
69
|
+
|
|
70
|
+
// remember last fetch config and search so the child can refetch/append by itself
|
|
71
|
+
const lastPack = ref<FetchPack | null>(null); // { url, data, params, mapFn }
|
|
72
|
+
const lastSearch = ref(""); // current search term
|
|
73
|
+
|
|
74
|
+
const hasNextPage = computed(() => {
|
|
75
|
+
const total = Number((fieldData.value as { total?: number; }).total ?? 0);
|
|
76
|
+
return field.all.length < total;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ask parent for a pack and run our internal fetcher
|
|
80
|
+
const callParentFetched = async (search: string, opts: Partial<FetchPack> = {}) => {
|
|
81
|
+
const maybePack = await props.fetched?.({ search, ...opts });
|
|
82
|
+
if (maybePack && typeof maybePack === "object") {
|
|
83
|
+
await fetchedDropdown(search, { ...maybePack, ...opts });
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const onClear = () => {
|
|
88
|
+
resetMe(); // clear options + selected value
|
|
89
|
+
skipNextSearch.value = true; // suppress the immediate empty search
|
|
90
|
+
emit("clear"); // notify parent that clear button was clicked
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const onOpen = async () => {
|
|
94
|
+
// if empty, load page 1 via parent fetcher (no boolean flags)
|
|
95
|
+
if (!field.all.length) {
|
|
96
|
+
if (lastPack.value) {
|
|
97
|
+
await fetchedDropdown("", { ...lastPack.value, reset: true });
|
|
98
|
+
} else {
|
|
99
|
+
// await props.fetched?.('');
|
|
100
|
+
await callParentFetched("", { reset: true });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
await nextTick();
|
|
105
|
+
if (load.value && hasNextPage.value) {
|
|
106
|
+
observer.value?.observe(load.value);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const onClose = async () => {
|
|
111
|
+
observer.value?.disconnect();
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const onModelUpdate = async (val: unknown) => {
|
|
115
|
+
const optHooks = Array.isArray(hooks.value.option)
|
|
116
|
+
? hooks.value.option
|
|
117
|
+
: hooks.value.option
|
|
118
|
+
? [hooks.value.option]
|
|
119
|
+
: [];
|
|
120
|
+
|
|
121
|
+
// When user clicks × or backspaces everything, v-select sets model to null.
|
|
122
|
+
if (val == null || val === "") {
|
|
123
|
+
for (const f of optHooks) {
|
|
124
|
+
if (typeof f === "function") {
|
|
125
|
+
await Promise.resolve(f(null));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
emit("clear");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
field.val = val as SelectValue;
|
|
133
|
+
|
|
134
|
+
for (const f of optHooks) {
|
|
135
|
+
if (typeof f === "function") {
|
|
136
|
+
await Promise.resolve(f(val));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const inputSearch = debounce(async (search: string, loading: (value: boolean) => void) => {
|
|
142
|
+
if (skipNextSearch.value && (!search || !search.trim().length)) {
|
|
143
|
+
skipNextSearch.value = false; // consume the flag
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (search && search.trim().length) {
|
|
148
|
+
loading(true);
|
|
149
|
+
if (lastPack.value) {
|
|
150
|
+
await fetchedDropdown(search, { ...lastPack.value, reset: true }).finally(() =>
|
|
151
|
+
loading(false)
|
|
152
|
+
);
|
|
153
|
+
} else {
|
|
154
|
+
await callParentFetched(search, { reset: true }).finally(() => loading(false));
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
// user erased the query manually
|
|
158
|
+
field.page = 0;
|
|
159
|
+
if (lastPack.value) {
|
|
160
|
+
await fetchedDropdown("", { ...lastPack.value }); // no reset → keep list
|
|
161
|
+
} else {
|
|
162
|
+
await callParentFetched("");
|
|
163
|
+
}
|
|
164
|
+
await nextTick();
|
|
165
|
+
if (load.value && hasNextPage.value) observer.value?.observe(load.value);
|
|
166
|
+
}
|
|
167
|
+
}, 400);
|
|
168
|
+
|
|
169
|
+
const infiniteScroll = async ([{ isIntersecting, target }]: IntersectionObserverEntry[]) => {
|
|
170
|
+
if (!isIntersecting) return;
|
|
171
|
+
const ul =
|
|
172
|
+
target instanceof HTMLElement && target.offsetParent instanceof HTMLElement
|
|
173
|
+
? target.offsetParent
|
|
174
|
+
: null;
|
|
175
|
+
const scrollTop = ul?.scrollTop ?? 0;
|
|
176
|
+
if (lastPack.value) {
|
|
177
|
+
await fetchedDropdown(lastSearch.value, { ...lastPack.value }); // next page of current mode
|
|
178
|
+
} else {
|
|
179
|
+
await callParentFetched(lastSearch.value || "");
|
|
180
|
+
}
|
|
181
|
+
await nextTick();
|
|
182
|
+
if (ul) ul.scrollTop = scrollTop;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
onMounted(async () => {
|
|
186
|
+
observer.value = new IntersectionObserver(infiniteScroll);
|
|
187
|
+
|
|
188
|
+
// set default value if provided
|
|
189
|
+
if (props.defaultValue) {
|
|
190
|
+
await nextTick();
|
|
191
|
+
field.val = props.defaultValue;
|
|
192
|
+
model.value = props.defaultValue;
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
onBeforeUnmount(() => {
|
|
197
|
+
observer.value?.disconnect();
|
|
198
|
+
observer.value = null;
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// prototype and it will call from parent
|
|
202
|
+
const field = reactive<{ all: AnyRecord[]; val: SelectValue; page: number; size: number; }>({
|
|
203
|
+
all: [],
|
|
204
|
+
val: "",
|
|
205
|
+
page: 0,
|
|
206
|
+
size: 10
|
|
207
|
+
});
|
|
208
|
+
const fieldData = ref<AnyRecord>({});
|
|
209
|
+
|
|
210
|
+
const resetMe = () => {
|
|
211
|
+
field.all = [];
|
|
212
|
+
field.page = 0;
|
|
213
|
+
field.val = "";
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const reloadMe = () => {
|
|
217
|
+
field.all = [];
|
|
218
|
+
field.page = 0;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const fetchedDropdown = async (search: string, pack: FetchPack | null = null) => {
|
|
222
|
+
const url = pack?.url ?? null;
|
|
223
|
+
const data = pack?.data ?? null;
|
|
224
|
+
const params = pack?.params ?? {};
|
|
225
|
+
const mapFn = pack?.mapFn ?? ((x: AnyRecord) => x);
|
|
226
|
+
const reset = !!pack?.reset;
|
|
227
|
+
const reload = !!pack?.reload;
|
|
228
|
+
|
|
229
|
+
// make option sticky: only overwrite if caller provided it
|
|
230
|
+
if (Object.prototype.hasOwnProperty.call(pack ?? {}, "option")) {
|
|
231
|
+
const pOpt = pack?.option;
|
|
232
|
+
hooks.value.option = Array.isArray(pOpt)
|
|
233
|
+
? pOpt.filter((fn) => typeof fn === "function")
|
|
234
|
+
: typeof pOpt === "function"
|
|
235
|
+
? [pOpt]
|
|
236
|
+
: [];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// remember essentials so the child can refetch/append on its own
|
|
240
|
+
lastPack.value = { url, data, params, mapFn };
|
|
241
|
+
lastSearch.value = search ?? "";
|
|
242
|
+
|
|
243
|
+
// if pass and true -> reset, reload , search
|
|
244
|
+
(reset && resetMe()) ||
|
|
245
|
+
(reload && reloadMe()) ||
|
|
246
|
+
(!empty(search) && !reset && !reload && reloadMe());
|
|
247
|
+
|
|
248
|
+
// after reset/reload/search
|
|
249
|
+
field.page++;
|
|
250
|
+
const param = { page: field.page, size: field.size, search: search };
|
|
251
|
+
|
|
252
|
+
// log once (don't call axios twice)
|
|
253
|
+
// console.log('GET', pack.url, { params: pack.params ? { ...pack.params, ...param } : param });
|
|
254
|
+
|
|
255
|
+
if (!url || !data) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const key = await axios.get(url, { params: params ? { ...params, ...param } : param });
|
|
260
|
+
const payload = key.data[data as string] as AnyRecord;
|
|
261
|
+
|
|
262
|
+
fieldData.value = payload;
|
|
263
|
+
const rows = Array.isArray((payload as { data?: unknown; }).data)
|
|
264
|
+
? (payload as { data: AnyRecord[]; }).data
|
|
265
|
+
: [];
|
|
266
|
+
// console.log('Fetched', payload?.data);
|
|
267
|
+
rows.forEach((dt) => {
|
|
268
|
+
const opt = mapFn(dt);
|
|
269
|
+
const key = opt?.id ?? opt?.title ?? JSON.stringify(opt); // build a comparison key: prefer id, then title, then full object
|
|
270
|
+
|
|
271
|
+
if (
|
|
272
|
+
!field.all.some(
|
|
273
|
+
(o) =>
|
|
274
|
+
((o as { id?: unknown; title?: unknown; }).id ??
|
|
275
|
+
(o as { id?: unknown; title?: unknown; }).title ??
|
|
276
|
+
JSON.stringify(o)) === key
|
|
277
|
+
)
|
|
278
|
+
) {
|
|
279
|
+
field.all = [...field.all, opt];
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* This function created for
|
|
286
|
+
* functionally reset or reload
|
|
287
|
+
* from without component
|
|
288
|
+
* like fn.value.reload({reset:true/reload:true/both});
|
|
289
|
+
* it is exposed, so we can call from with
|
|
290
|
+
* template ref
|
|
291
|
+
*/
|
|
292
|
+
const reload = async () => {
|
|
293
|
+
resetMe(); // clear field.all, page, val
|
|
294
|
+
await callParentFetched("", { reset: true }); // ask parent to fetch first page again
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* if backend send proper object or null
|
|
299
|
+
* then watch function not get any axios
|
|
300
|
+
* error else axios can send error
|
|
301
|
+
*/
|
|
302
|
+
watch(
|
|
303
|
+
() => props.resetKey,
|
|
304
|
+
async (newVal, oldVal) => {
|
|
305
|
+
if (oldVal === undefined) return;
|
|
306
|
+
|
|
307
|
+
const optHooks = Array.isArray(hooks.value.option)
|
|
308
|
+
? hooks.value.option
|
|
309
|
+
: hooks.value.option
|
|
310
|
+
? [hooks.value.option]
|
|
311
|
+
: [];
|
|
312
|
+
|
|
313
|
+
resetMe(); // clears field.all, field.page, field.val
|
|
314
|
+
model.value = "";
|
|
315
|
+
|
|
316
|
+
// Notify hooks that selection is gone
|
|
317
|
+
for (const fn of optHooks) {
|
|
318
|
+
if (typeof fn === "function") {
|
|
319
|
+
await Promise.resolve(fn(null));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
skipNextSearch.value = true; // avoid instant empty-search call
|
|
324
|
+
await callParentFetched("", { reset: true, reload: true });
|
|
325
|
+
}
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
defineExpose({ field, fieldData, fetchedDropdown, reload });
|
|
329
|
+
|
|
330
|
+
const parentClass = computed(() => ($attrs.parentclass as string | undefined) || "mb-2");
|
|
331
|
+
const inputId = computed(() => ($attrs.id as string | undefined) || "");
|
|
332
|
+
const inputTitle = computed(() => ($attrs.title as string | undefined) || "");
|
|
333
|
+
const hoodHtml = computed(() =>
|
|
334
|
+
props.hood === false || props.hood == null ? "" : String(props.hood)
|
|
335
|
+
);
|
|
336
|
+
</script>
|
|
337
|
+
|
|
338
|
+
<template>
|
|
339
|
+
<div :class="parentClass">
|
|
340
|
+
<label
|
|
341
|
+
:for="inputId"
|
|
342
|
+
class="text-capitalize d-flex align-items-center mb-1"
|
|
343
|
+
:class="{ 'd-none': !inputTitle }">
|
|
344
|
+
<div class="position-relative">
|
|
345
|
+
<span class="text-capitalize" v-html="inputTitle"></span>
|
|
346
|
+
<span
|
|
347
|
+
class="position-absolute text-danger rounded-circle bg-danger must"
|
|
348
|
+
:class="{ 'd-none': !props.must }"></span>
|
|
349
|
+
</div>
|
|
350
|
+
<div
|
|
351
|
+
class="text-uppercase w-100 text-end text-primary fw-semibold"
|
|
352
|
+
:class="{ 'd-none': !props.hood }"
|
|
353
|
+
style="font-size: 12px; margin-top: 0.15rem"
|
|
354
|
+
v-html="hoodHtml"></div>
|
|
355
|
+
</label>
|
|
356
|
+
<v-select
|
|
357
|
+
v-model="model"
|
|
358
|
+
label="title"
|
|
359
|
+
:filterable="false"
|
|
360
|
+
:options="field.all"
|
|
361
|
+
v-bind="$attrs"
|
|
362
|
+
@open="onOpen"
|
|
363
|
+
@update:model-value="onModelUpdate"
|
|
364
|
+
@close="onClose"
|
|
365
|
+
@search="inputSearch"
|
|
366
|
+
@clear="onClear">
|
|
367
|
+
<template #list-footer>
|
|
368
|
+
<li v-show="hasNextPage" ref="load" class="loader">More...</li>
|
|
369
|
+
</template>
|
|
370
|
+
</v-select>
|
|
371
|
+
<div class="form-text text-danger" :class="{ 'd-none': !props.err }" style="font-size: 0.8rem">
|
|
372
|
+
{{ props.err }}
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
</template>
|
|
376
|
+
|
|
377
|
+
<style lang="scss" scoped>
|
|
378
|
+
.loader {
|
|
379
|
+
text-align: center;
|
|
380
|
+
color: #bbbbbb;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.must {
|
|
384
|
+
width: 4px;
|
|
385
|
+
height: 4px;
|
|
386
|
+
top: 0;
|
|
387
|
+
margin-top: 5px;
|
|
388
|
+
margin-left: 2px;
|
|
389
|
+
}
|
|
390
|
+
</style>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="h-100" v-bind="$attrs">
|
|
3
|
+
<div class="h-100 d-flex align-items-center justify-content-center">
|
|
4
|
+
<div class="spinner-grow" :style="spinnerStyle" role="status">
|
|
5
|
+
<span class="visually-hidden">Loading...</span>
|
|
6
|
+
</div>
|
|
7
|
+
<div class="fw-bold ms-2">
|
|
8
|
+
<span v-if="props.text" :class="props.textsize" :style="textStyle">
|
|
9
|
+
{{ props.text }}
|
|
10
|
+
</span>
|
|
11
|
+
<span v-else>Loading...</span>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script setup lang="ts">
|
|
18
|
+
import { computed } from "vue";
|
|
19
|
+
|
|
20
|
+
defineOptions({ name: "Spinner", inheritAttrs: false });
|
|
21
|
+
|
|
22
|
+
interface SpinnerProps {
|
|
23
|
+
size?: string;
|
|
24
|
+
text?: string;
|
|
25
|
+
textsize?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const props = withDefaults(defineProps<SpinnerProps>(), {
|
|
29
|
+
size: "3rem",
|
|
30
|
+
text: "",
|
|
31
|
+
textsize: ""
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const spinnerStyle = computed(() => ({
|
|
35
|
+
width: props.size,
|
|
36
|
+
height: props.size
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
const textStyle = computed(() => ({
|
|
40
|
+
fontSize: props.textsize || undefined
|
|
41
|
+
}));
|
|
42
|
+
</script>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="wrapperClass">
|
|
3
|
+
<label :class="props.vertical ? 'form-check-label d-block' : 'd-none'">
|
|
4
|
+
<span v-html="statusHtml"></span>
|
|
5
|
+
</label>
|
|
6
|
+
<div class="form-check form-switch d-inline-block">
|
|
7
|
+
<input
|
|
8
|
+
class="form-check-input shadow-none border-primary"
|
|
9
|
+
type="checkbox"
|
|
10
|
+
role="switch"
|
|
11
|
+
:checked="isChecked"
|
|
12
|
+
v-bind="$attrs" />
|
|
13
|
+
<label :class="props.vertical ? 'd-none' : 'form-check-label'">
|
|
14
|
+
<span v-html="statusHtml"></span>
|
|
15
|
+
</label>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script setup lang="ts">
|
|
21
|
+
import { computed, useAttrs } from "vue";
|
|
22
|
+
|
|
23
|
+
defineOptions({ name: "Switch", inheritAttrs: false });
|
|
24
|
+
|
|
25
|
+
interface SwitchProps {
|
|
26
|
+
modelValue?: string | number | boolean;
|
|
27
|
+
value?: unknown[] | Record<string, unknown> | string | number | boolean;
|
|
28
|
+
vertical?: boolean;
|
|
29
|
+
checked?: boolean | number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const $attrs = useAttrs();
|
|
33
|
+
const props = defineProps<SwitchProps>();
|
|
34
|
+
|
|
35
|
+
const textLabel = computed(() => ($attrs.text as string | undefined) || "");
|
|
36
|
+
const blankLabel = computed(() => ($attrs.blank as string | undefined) || "");
|
|
37
|
+
const topclass = computed(() => ($attrs.topclass as string | undefined) || "");
|
|
38
|
+
|
|
39
|
+
const isChecked = computed(() => Boolean(props.checked));
|
|
40
|
+
|
|
41
|
+
const wrapperClass = computed(() => (props.vertical ? `text-center ${topclass.value}`.trim() : ""));
|
|
42
|
+
|
|
43
|
+
const statusHtml = computed(() => {
|
|
44
|
+
if (isChecked.value) {
|
|
45
|
+
return `<div class='d-flex justify-content-center'><span>${textLabel.value}</span><div class='rounded-circle bg-success indicator'></div></div>`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return `<div class='d-flex justify-content-center'><span>${blankLabel.value}</span><div class='rounded-circle bg-danger indicator'></div></div>`;
|
|
49
|
+
});
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<style lang="scss">
|
|
53
|
+
.indicator {
|
|
54
|
+
width: 4px;
|
|
55
|
+
height: 4px;
|
|
56
|
+
margin-left: 2px;
|
|
57
|
+
margin-top: 5px;
|
|
58
|
+
}
|
|
59
|
+
</style>
|
|
60
|
+
<style lang="scss" scoped>
|
|
61
|
+
label,
|
|
62
|
+
input {
|
|
63
|
+
cursor: pointer;
|
|
64
|
+
}
|
|
65
|
+
</style>
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="wrapperClass">
|
|
3
|
+
<div class="position-relative">
|
|
4
|
+
<div class="position-relative mb-1">
|
|
5
|
+
<span class="text-capitalize" v-html="inputLabel"></span>
|
|
6
|
+
<span
|
|
7
|
+
class="position-absolute text-danger rounded-circle bg-danger must"
|
|
8
|
+
:class="{ 'd-none': !props.must }"></span>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="form-floating h-100">
|
|
11
|
+
<textarea
|
|
12
|
+
:value="props.modelValue"
|
|
13
|
+
class="form-control mb-0 h-100"
|
|
14
|
+
:maxlength="maxLengthAttr"
|
|
15
|
+
:aria-describedby="`${inputId}Help`"
|
|
16
|
+
v-bind="$attrs"
|
|
17
|
+
@input="updateModel">
|
|
18
|
+
</textarea>
|
|
19
|
+
<label
|
|
20
|
+
:for="inputId"
|
|
21
|
+
class="d-block placeholder-label border-0"
|
|
22
|
+
:class="{ 'd-none': !placeholderText }">
|
|
23
|
+
{{ placeholderText }}
|
|
24
|
+
</label>
|
|
25
|
+
</div>
|
|
26
|
+
<label
|
|
27
|
+
class="position-absolute end-0 pe-1 text-secondary max"
|
|
28
|
+
:class="{ 'd-none': !max }"
|
|
29
|
+
style="top: 28px">
|
|
30
|
+
{{ maxCounter }}
|
|
31
|
+
</label>
|
|
32
|
+
</div>
|
|
33
|
+
<div :id="`${inputId}Help`" class="form-text text-danger" :class="{ 'd-none': !props.err }">
|
|
34
|
+
{{ props.err }}
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<script setup lang="ts">
|
|
40
|
+
import { computed, nextTick, onMounted, useAttrs } from "vue";
|
|
41
|
+
|
|
42
|
+
defineOptions({ name: "TextArea", inheritAttrs: false });
|
|
43
|
+
|
|
44
|
+
const $attrs = useAttrs();
|
|
45
|
+
interface TextAreaProps {
|
|
46
|
+
focus?: boolean;
|
|
47
|
+
must?: boolean;
|
|
48
|
+
err?: string | boolean;
|
|
49
|
+
modelValue?: string | number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const props = withDefaults(defineProps<TextAreaProps>(), {
|
|
53
|
+
modelValue: ""
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const inputId = computed(() => ($attrs.id as string | undefined) || "");
|
|
57
|
+
const inputLabel = computed(() => ($attrs.label as string | undefined) || "");
|
|
58
|
+
const placeholderText = computed(() => ($attrs.placeholder as string | undefined) || "");
|
|
59
|
+
const topclass = computed(() => ($attrs.topclass as string | undefined) || "");
|
|
60
|
+
|
|
61
|
+
const wrapperClass = computed(() =>
|
|
62
|
+
topclass.value ? `${topclass.value} position-relative` : "mb-2"
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const max = computed<number | null>(() => {
|
|
66
|
+
const raw = $attrs.maxlength as string | number | undefined;
|
|
67
|
+
return raw != null ? Number(raw) : null;
|
|
68
|
+
});
|
|
69
|
+
const maxLengthAttr = computed<number | undefined>(() => max.value ?? undefined);
|
|
70
|
+
const maxCounter = computed(() =>
|
|
71
|
+
max.value == null ? "" : max.value - String(props.modelValue).length
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const emit = defineEmits<{
|
|
75
|
+
(event: "update:modelValue", value: string): void;
|
|
76
|
+
}>();
|
|
77
|
+
const updateModel = (e: Event) => {
|
|
78
|
+
const target = e.target as HTMLTextAreaElement;
|
|
79
|
+
emit("update:modelValue", target.value);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
onMounted(() => {
|
|
83
|
+
if (!props.focus || !inputId.value) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
nextTick(() => {
|
|
88
|
+
const textarea = document.querySelector(`#${inputId.value}`) as HTMLTextAreaElement | null;
|
|
89
|
+
textarea?.focus();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
</script>
|
|
93
|
+
|
|
94
|
+
<style lang="scss" scoped>
|
|
95
|
+
.form-floating {
|
|
96
|
+
label {
|
|
97
|
+
top: -1px;
|
|
98
|
+
left: 1px;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
textarea:focus + label,
|
|
102
|
+
textarea:not(:placeholder-shown) + label {
|
|
103
|
+
top: -6px;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.placeholder-label {
|
|
108
|
+
color: #cccccc !important;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.must {
|
|
112
|
+
width: 4px;
|
|
113
|
+
height: 4px;
|
|
114
|
+
margin-left: 2px;
|
|
115
|
+
margin-top: 5px;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.max {
|
|
119
|
+
font-size: 0.6rem;
|
|
120
|
+
}
|
|
121
|
+
</style>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Teleport to="#toast-show">
|
|
3
|
+
<div
|
|
4
|
+
ref="notify"
|
|
5
|
+
class="toast position-fixed"
|
|
6
|
+
role="alert"
|
|
7
|
+
aria-live="assertive"
|
|
8
|
+
aria-atomic="true"
|
|
9
|
+
style="z-index: 1000000; top: 10px; right: 23px"
|
|
10
|
+
data-bs-delay="2000">
|
|
11
|
+
<div :class="`text-bg-${bgcolor} align-items-center rounded-1 shadow-sm`">
|
|
12
|
+
<div class="d-flex">
|
|
13
|
+
<div class="toast-body">
|
|
14
|
+
{{ message }}
|
|
15
|
+
</div>
|
|
16
|
+
<button
|
|
17
|
+
type="button"
|
|
18
|
+
class="btn-close btn-close-white me-2 m-auto"
|
|
19
|
+
data-bs-dismiss="toast"
|
|
20
|
+
aria-label="Close"></button>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</Teleport>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<script setup lang="ts">
|
|
28
|
+
import { ref } from "vue";
|
|
29
|
+
import { Toast } from "bootstrap";
|
|
30
|
+
|
|
31
|
+
defineOptions({ name: "Toast", inheritAttrs: false });
|
|
32
|
+
|
|
33
|
+
interface ToastProps {
|
|
34
|
+
icon?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
defineProps<ToastProps>();
|
|
38
|
+
|
|
39
|
+
const notify = ref<HTMLElement | null>(null);
|
|
40
|
+
const message = ref("");
|
|
41
|
+
const bgcolor = ref("primary");
|
|
42
|
+
|
|
43
|
+
const toastme = (msg: string, bg: string) => {
|
|
44
|
+
message.value = msg;
|
|
45
|
+
bgcolor.value = bg;
|
|
46
|
+
if (!notify.value) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const toast = Toast.getOrCreateInstance(notify.value);
|
|
50
|
+
toast.show();
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
defineExpose({ toastme });
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<style lang="scss" scoped></style>
|