updated
10
index.html
|
|
@ -5,16 +5,6 @@
|
|||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>HRMS - Landing Page</title>
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
#viewDiv {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
|
|||
18
package.json
|
|
@ -11,26 +11,31 @@
|
|||
"format": "prettier ./src --write"
|
||||
},
|
||||
"dependencies": {
|
||||
"@arcgis/core": "^4.28.10",
|
||||
"@fullcalendar/core": "^6.1.8",
|
||||
"@fullcalendar/daygrid": "^6.1.8",
|
||||
"@fullcalendar/interaction": "^6.1.8",
|
||||
"@fullcalendar/list": "^6.1.8",
|
||||
"@fullcalendar/timegrid": "^6.1.8",
|
||||
"@fullcalendar/vue3": "^6.1.8",
|
||||
"@googlemaps/js-api-loader": "^1.16.2",
|
||||
"@quasar/extras": "^1.15.8",
|
||||
"@vuepic/vue-datepicker": "^5.2.1",
|
||||
"keycloak-js": "^22.0.2",
|
||||
"moment": "^2.29.4",
|
||||
"pinia": "^2.1.4",
|
||||
"quasar": "^2.17.5",
|
||||
"quasar": "^2.11.1",
|
||||
"register-service-worker": "^1.7.2",
|
||||
"sass": "^1.83.0",
|
||||
"simple-vue-camera": "^1.1.3",
|
||||
"vite-plugin-pwa": "^0.16.7",
|
||||
"vue": "^3.4.15",
|
||||
"vue-router": "^4.5.0"
|
||||
"vue-router": "^4.1.6",
|
||||
"vue3-google-map": "^0.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@quasar/vite-plugin": "^1.3.0",
|
||||
"@rushstack/eslint-patch": "^1.1.4",
|
||||
"@types/jsdom": "^20.0.1",
|
||||
"@types/node": "^18.18.10",
|
||||
"@types/vue-router": "^2.0.0",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"@vitejs/plugin-vue-jsx": "^3.0.0",
|
||||
"@vue/eslint-config-prettier": "^7.0.0",
|
||||
|
|
@ -44,10 +49,13 @@
|
|||
"jsdom": "^20.0.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.7.1",
|
||||
"sass": "^1.83.0",
|
||||
"sass-loader": "^16.0.4",
|
||||
"start-server-and-test": "^1.15.2",
|
||||
"typescript": "~4.7.4",
|
||||
"vite": "^4.0.0",
|
||||
"vitest": "^0.25.6",
|
||||
"vue-loader": "^17.4.2",
|
||||
"vue-tsc": "^1.0.12"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1 MiB |
BIN
src/assets/3135715.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
src/assets/bg.png
Normal file
|
After Width: | Height: | Size: 391 KiB |
BIN
src/assets/line2.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/assets/logo.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src/assets/screen1.png
Normal file
|
After Width: | Height: | Size: 154 KiB |
BIN
src/assets/screen2.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
src/assets/screen3.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
src/assets/screen4.png
Normal file
|
After Width: | Height: | Size: 101 KiB |
90
src/components/CustomDialog.vue
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
<template>
|
||||
<q-dialog ref="dialogRef" @hide="onDialogHide" persistent>
|
||||
<q-card class="q-pa-sm">
|
||||
<q-card-section class="row">
|
||||
<div class="q-pr-md">
|
||||
<q-avatar
|
||||
:icon="icon"
|
||||
size="lg"
|
||||
font-size="25px"
|
||||
color="blue-1"
|
||||
:text-color="color"
|
||||
/>
|
||||
</div>
|
||||
<div class="col text-dark">
|
||||
<span class="text-bold">{{ title }}</span>
|
||||
<br />
|
||||
<span>{{ message }}</span>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions
|
||||
align="right"
|
||||
class="bg-white text-teal"
|
||||
v-if="onlycancel"
|
||||
>
|
||||
<q-btn label="ตกลง" flat color="grey-8" @click="onDialogCancel" />
|
||||
<!-- <q-btn :label="textOk" :color="color" @click="onOKClick" /> -->
|
||||
</q-card-actions>
|
||||
<q-card-actions align="right" class="bg-white text-teal" v-else>
|
||||
<q-btn label="ยกเลิก" flat color="grey-8" @click="onDialogCancel" />
|
||||
<q-btn :label="textOk" :color="color" @click="onOKClick" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDialogPluginComponent } from "quasar";
|
||||
|
||||
const props = defineProps({
|
||||
color: {
|
||||
type: String,
|
||||
default: "primary",
|
||||
},
|
||||
textOk: {
|
||||
type: String,
|
||||
default: "ตกลง",
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: "หัวข้อ?",
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
default: "ข้อความ",
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: "question_mark",
|
||||
},
|
||||
onlycancel: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
defineEmits([
|
||||
// REQUIRED; need to specify some events that your
|
||||
// component will emit through useDialogPluginComponent()
|
||||
...useDialogPluginComponent.emits,
|
||||
]);
|
||||
|
||||
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
|
||||
useDialogPluginComponent();
|
||||
// dialogRef - Vue ref to be applied to QDialog
|
||||
// onDialogHide - Function to be used as handler for @hide on QDialog
|
||||
// onDialogOK - Function to call to settle dialog with "ok" outcome
|
||||
// example: onDialogOK() - no payload
|
||||
// example: onDialogOK({ /*...*/ }) - with payload
|
||||
// onDialogCancel - Function to call to settle dialog with "cancel" outcome
|
||||
|
||||
// this is part of our example (so not required)
|
||||
function onOKClick() {
|
||||
// on OK, it is REQUIRED to
|
||||
// call onDialogOK (with optional payload)
|
||||
onDialogOK();
|
||||
// or with payload: onDialogOK({ ... })
|
||||
// ...and it will also hide the dialog automatically
|
||||
}
|
||||
</script>
|
||||
27
src/plugins/cookie.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// authen with keycloak client
|
||||
function setCookie(name: string, value: string, days: number) {
|
||||
let expires = "";
|
||||
if (days) {
|
||||
const date = new Date();
|
||||
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
|
||||
expires = "; expires=" + date.toUTCString();
|
||||
}
|
||||
document.cookie = name + "=" + (value || "") + expires + "; path=/";
|
||||
}
|
||||
|
||||
function getCookie(name: string) {
|
||||
const nameEQ = name + "=";
|
||||
const ca = document.cookie.split(";");
|
||||
for (let i = 0; i < ca.length; i++) {
|
||||
let c = ca[i];
|
||||
while (c.charAt(0) == " ") c = c.substring(1, c.length);
|
||||
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function deleteCookie(name: string) {
|
||||
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
||||
}
|
||||
|
||||
export { setCookie, getCookie, deleteCookie };
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
// authen with keycloak client
|
||||
import Keycloak from "keycloak-js";
|
||||
|
||||
// const ACCESS_TOKEN = 'BMAHRIS_KEYCLOAK_IDENTITY'
|
||||
// const REFRESH_TOKEN = 'BMAHRIS_KEYCLOAK_REFRESH'
|
||||
// const keycloakConfig = {
|
||||
// url: import.meta.env.VITE_URL_KEYCLOAK,
|
||||
// realm: import.meta.env.VITE_REALM_KEYCLOAK,
|
||||
// clientId: import.meta.env.VITE_CLIENTID_KEYCLOAK,
|
||||
// clientSecret: import.meta.env.VITE_CLIENTSECRET_KEYCLOAK,
|
||||
// }
|
||||
|
||||
// const keycloak = new Keycloak(keycloakConfig)
|
||||
|
||||
// async function kcAuthen(access_token: string, refresh_token: string) {
|
||||
// await setCookie(ACCESS_TOKEN, access_token, 1)
|
||||
// await setCookie(REFRESH_TOKEN, refresh_token, 1)
|
||||
// window.location.href = '/'
|
||||
// }
|
||||
|
||||
// async function kcLogout() {
|
||||
// await deleteCookie(ACCESS_TOKEN)
|
||||
// await deleteCookie(REFRESH_TOKEN)
|
||||
// if (keycloak.authenticated !== undefined) {
|
||||
// keycloak.logout()
|
||||
// }
|
||||
// window.location.href = '/login'
|
||||
// }
|
||||
|
||||
// async function getToken() {
|
||||
// return {
|
||||
// token: getCookie(ACCESS_TOKEN),
|
||||
// refresh_token: getCookie(REFRESH_TOKEN),
|
||||
// }
|
||||
// }
|
||||
|
||||
// function setCookie(name: string, value: any, days: number) {
|
||||
// let expires = ''
|
||||
// if (days) {
|
||||
// const date = new Date()
|
||||
// date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000)
|
||||
// expires = '; expires=' + date.toUTCString()
|
||||
// }
|
||||
// document.cookie = name + '=' + (value || '') + expires + '; path=/'
|
||||
// }
|
||||
|
||||
// function getCookie(name: string) {
|
||||
// const nameEQ = name + '='
|
||||
// const ca = document.cookie.split(';')
|
||||
// for (let i = 0; i < ca.length; i++) {
|
||||
// let c = ca[i]
|
||||
// while (c.charAt(0) == ' ') c = c.substring(1, c.length)
|
||||
// if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length)
|
||||
// }
|
||||
// return null
|
||||
// }
|
||||
|
||||
// function deleteCookie(name: string) {
|
||||
// document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`
|
||||
// }
|
||||
|
||||
// export default keycloak
|
||||
// export {
|
||||
// keycloakConfig,
|
||||
// getToken,
|
||||
// kcAuthen,
|
||||
// kcLogout,
|
||||
// ACCESS_TOKEN,
|
||||
// REFRESH_TOKEN,
|
||||
// }
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { createRouter, createWebHistory } from "vue-router";
|
||||
const homeView = () => import("@/views/home.vue");
|
||||
const ssoPage = () => import("@/views/sso.vue");
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
|
|
@ -23,6 +24,14 @@ const router = createRouter({
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/sso",
|
||||
name: "sso-page",
|
||||
component: ssoPage,
|
||||
meta: {
|
||||
Auth: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,173 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted } from "vue";
|
||||
import { deleteCookie, getCookie, setCookie } from "@/plugins/cookie";
|
||||
import router from "@/router";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useQuasar } from "quasar";
|
||||
import axios from "axios";
|
||||
|
||||
import CustomComponent from "@/components/CustomDialog.vue";
|
||||
|
||||
const $q = useQuasar();
|
||||
|
||||
const urlAdmin = import.meta.env.VITE_URL_ADMIN ?? "";
|
||||
const urlUser = import.meta.env.VITE_URL_USER ?? "";
|
||||
const urlMgt = import.meta.env.VITE_URL_MGT ?? "";
|
||||
const urlCheckin = import.meta.env.VITE_URL_CHECKIN ?? "";
|
||||
|
||||
const token = ref<any>("");
|
||||
const refreshToken = ref<any>("");
|
||||
const fullname = computed(() => {
|
||||
if (token.value) {
|
||||
const base64Url = token.value.split(".")[1];
|
||||
|
||||
// แปลงจาก Base64 URL-safe เป็น Base64 ปกติ
|
||||
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
||||
|
||||
// ถอดรหัส Base64
|
||||
const decoded = atob(base64);
|
||||
const decodedData = JSON.parse(decoded);
|
||||
|
||||
// ดึงชื่อผู้ใช้
|
||||
return decodeURIComponent(escape(decodedData.name));
|
||||
} else return "";
|
||||
});
|
||||
|
||||
async function goPage(sys: string, url: string) {
|
||||
// แยกส่วน Payload ของ JWT (ส่วนที่ 2)
|
||||
const base64Url = token.value.split(".")[1];
|
||||
|
||||
// แปลงจาก Base64 URL-safe เป็น Base64 ปกติ
|
||||
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
||||
|
||||
// ถอดรหัส Base64
|
||||
const decoded = atob(base64);
|
||||
|
||||
// กำหนด requiredRole ตามค่าของ sys
|
||||
let requiredRole: string[] = [];
|
||||
|
||||
if (sys === "user" || sys === "checkin") {
|
||||
requiredRole = ["USER"];
|
||||
} else if (sys === "mgt") {
|
||||
requiredRole = ["STAFF"]; // ถ้า sys เป็นค่าว่าง ให้ใช้ "ADMIN"
|
||||
} else if (sys === "admin") {
|
||||
requiredRole = ["ADMIN", "SUPER_ADMIN"];
|
||||
}
|
||||
|
||||
console.log("requiredRole===>", requiredRole);
|
||||
console.log("decoded===>", JSON.parse(decoded).realm_access.roles);
|
||||
|
||||
// ตรวจสอบว่า payload.role มีค่าหรือไม่ และว่ามี role ที่ต้องการหรือไม่
|
||||
if (
|
||||
requiredRole.some((role) =>
|
||||
JSON.parse(decoded).realm_access.roles.includes(role)
|
||||
)
|
||||
) {
|
||||
window.location.href = `${url}/auth?token=${token.value}&accessToken=${refreshToken.value}`;
|
||||
} else {
|
||||
// alert("คุณไม่มีสิทธิ์เข้าใช้งานระบบนี้");
|
||||
$q.dialog({
|
||||
component: CustomComponent,
|
||||
componentProps: {
|
||||
title: `แจ้งเตือน`,
|
||||
message: "คุณไม่มีสิทธิ์เข้าใช้งานระบบนี้",
|
||||
icon: "warning",
|
||||
color: "red",
|
||||
onlycancel: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
await deleteCookie("BMAHRIS_KEYCLOAK_IDENTITY");
|
||||
await deleteCookie("BMAHRIS_KEYCLOAK_REFRESH");
|
||||
|
||||
router.push("/sso");
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// const checkAuthen = await authenticated();
|
||||
// if (checkAuthen) {
|
||||
// router.push("/");
|
||||
// }
|
||||
token.value = await getCookie("BMAHRIS_KEYCLOAK_IDENTITY");
|
||||
refreshToken.value = await getCookie("BMAHRIS_KEYCLOAK_REFRESH");
|
||||
|
||||
deleteCookie("BMAHRISADM_KEYCLOAK_IDENTITY");
|
||||
deleteCookie("BMAHRISCKI_KEYCLOAK_IDENTITY");
|
||||
deleteCookie("BMAHRISUSER_KEYCLOAK_IDENTITY");
|
||||
|
||||
const checkToken = (await token.value) ?? null;
|
||||
|
||||
if (!checkToken && !token.value) {
|
||||
await axios
|
||||
.post(
|
||||
`${import.meta.env.VITE_SSO_URL}/kcauth`,
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
withCredentials: true, // Include cookies with the request
|
||||
}
|
||||
)
|
||||
.then((res: any) => {
|
||||
console.log("res===>", res);
|
||||
|
||||
if (res.status === 200) {
|
||||
setCookie("BMAHRIS_KEYCLOAK_IDENTITY", res.data.access_token, 1);
|
||||
setCookie("BMAHRIS_KEYCLOAK_REFRESH", res.data.refresh_token, 1);
|
||||
}
|
||||
})
|
||||
.catch((err: any) => {
|
||||
router.push("/sso");
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>Lanading page</h1>
|
||||
<div class="q-ma-md">
|
||||
<div class="row">
|
||||
{{ fullname }}
|
||||
<q-btn @click="logout()" class="btn_logout">
|
||||
ออกจากระบบ <i class="mdi mdi-logout"></i>
|
||||
</q-btn>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4">
|
||||
<img src="@/assets/screen1.png" style="width: 100%" />
|
||||
<div class="h-100 py-3">
|
||||
<a @click="goPage('user', urlUser)" class="link">
|
||||
ระบบบริการเจ้าของข้อมูล
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-4">
|
||||
<img src="@/assets/screen2.png" style="width: 100%" />
|
||||
<div class="h-100 py-3">
|
||||
<a @click="goPage('checkin', urlCheckin)" class="link">
|
||||
ระบบลงเวลาปฏิบัติราชการ
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-4">
|
||||
<img src="@/assets/screen3.png" style="width: 100%" />
|
||||
<div class="h-100 py-3">
|
||||
<a @click="goPage('mgt', urlMgt)" class="link"> ระบบบริหารจัดการ </a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-4">
|
||||
<img src="@/assets/screen4.png" style="width: 100%" />
|
||||
<div class="h-100 py-3">
|
||||
<a @click="goPage('admin', urlAdmin)" class="link"> ระบบแอดมิน </a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="scss" scoped>
|
||||
.link {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
298
src/views/sso.vue
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
<!-- authen with keycloak client -->
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useQuasar } from "quasar";
|
||||
import axios from "axios";
|
||||
|
||||
import { useRouter } from "vue-router";
|
||||
import { useCounterMixin } from "@/stores/mixin";
|
||||
|
||||
const $q = useQuasar();
|
||||
const router = useRouter();
|
||||
const mixin = useCounterMixin();
|
||||
|
||||
const { showLoader, hideLoader } = mixin;
|
||||
|
||||
const username = ref<string>(""); // ผู้ใช้
|
||||
const password = ref<string>(""); // รหัสผ่าน
|
||||
const msgError = ref<string>(""); // ข้อความแสดงข้อผิดพลาด
|
||||
/**
|
||||
* ฟังก์ชั่นเข้าสู่ระบบ
|
||||
*/
|
||||
async function onSubmit() {
|
||||
showLoader();
|
||||
msgError.value = "";
|
||||
// const formdata = new URLSearchParams();
|
||||
// formdata.append("username", username.value);
|
||||
// formdata.append("password", password.value);
|
||||
|
||||
await axios
|
||||
.post(
|
||||
`${import.meta.env.VITE_SSO_URL}/signin`,
|
||||
{
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
withCredentials: true, // Include cookies with the request
|
||||
}
|
||||
)
|
||||
.then((res) => {
|
||||
if (res.data === "OK") {
|
||||
router.push("/");
|
||||
}
|
||||
console.log("res===>", res);
|
||||
})
|
||||
.catch((err) => {
|
||||
msgError.value = "ชื่อผู้ใช้งานหรือรหัสผ่านไม่ถูกต้อง";
|
||||
})
|
||||
.finally(() => {
|
||||
hideLoader();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ทำงานเมื่อ components ถูกเรียกใช้งาน
|
||||
*/
|
||||
onMounted(async () => {});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-image">
|
||||
<div class="shadow-top"></div>
|
||||
<div class="shadow-bottom"></div>
|
||||
<div class="row fit items-center">
|
||||
<div class="row full-width justify-evenly">
|
||||
<div class="">
|
||||
<div id="myimage"></div>
|
||||
|
||||
<div class="text-logo">
|
||||
ระบบบริหารจัดการการใช้งาน <br />ระบบสารสนเทศสนับสนุนการเชื่อมโยง
|
||||
</div>
|
||||
<div class="subtext-logo">
|
||||
Bangkok Metropolitan Administration Single Sign-On
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img src="@/assets/line.png" class="img_absolute_line" />
|
||||
|
||||
<div class="card-pf">
|
||||
<div class="row q-gutter-sm items-center q-mb-sm">
|
||||
<div class="column">
|
||||
<div class="login-pf-header">
|
||||
<h1 id="kc-page-title">เข้าใช้งานระบบ</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<q-form
|
||||
greedy
|
||||
@submit.prevent
|
||||
@validation-success="onSubmit"
|
||||
style="max-width: 100%; min-width: 30%"
|
||||
>
|
||||
<div class="row">
|
||||
<div class="col-12 form-group">
|
||||
<label
|
||||
for="username"
|
||||
class="pf-c-form__label pf-c-form__label-text"
|
||||
>ชื่อผู้ใช้งาน</label
|
||||
>
|
||||
<q-input
|
||||
v-model="username"
|
||||
dense
|
||||
outlined
|
||||
lazy-rules
|
||||
hide-bottom-space
|
||||
:rules="[(val:string) => !!val || `${'กรุณากรอกชื่อผู้ใช้งาน'}`,]"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-12 form-group">
|
||||
<label
|
||||
for="username"
|
||||
class="pf-c-form__label pf-c-form__label-text"
|
||||
>รหัสผ่าน</label
|
||||
>
|
||||
<q-input
|
||||
v-model="password"
|
||||
dense
|
||||
outlined
|
||||
lazy-rules
|
||||
hide-bottom-space
|
||||
type="password"
|
||||
:rules="[(val:string) => !!val || `${'กรุณากรอกรหัสผ่าน'}`,]"
|
||||
></q-input>
|
||||
</div>
|
||||
|
||||
<div class="col-12 text-red text-center">{{ msgError }}</div>
|
||||
<div class="col-12">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
class="full-width"
|
||||
label="เข้าระบบ"
|
||||
style="font-size: 16px; border-radius: 8px"
|
||||
type="submit"
|
||||
>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</q-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.shadow-top {
|
||||
position: absolute;
|
||||
height: 4rem;
|
||||
width: 4rem;
|
||||
border-radius: 50%;
|
||||
background-color: $primary;
|
||||
top: -4rem;
|
||||
right: -4rem;
|
||||
opacity: 0.5;
|
||||
box-shadow: 0px 0px 300px 320px $primary;
|
||||
backdrop-filter: blur(550px);
|
||||
}
|
||||
.shadow-bottom {
|
||||
position: absolute;
|
||||
height: 4rem;
|
||||
width: 4rem;
|
||||
border-radius: 50%;
|
||||
background-color: $primary;
|
||||
bottom: -4rem;
|
||||
left: -4rem;
|
||||
opacity: 0.5;
|
||||
box-shadow: 0px 0px 300px 320px $primary;
|
||||
backdrop-filter: blur(550px);
|
||||
}
|
||||
|
||||
.color-icon {
|
||||
border-radius: 8px;
|
||||
}
|
||||
.link {
|
||||
text-decoration: none;
|
||||
color: #cc0004;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
color: #ff0004;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.checkbox,
|
||||
.radio {
|
||||
position: relative;
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
margin: 1px 3px 0 0;
|
||||
line-height: normal;
|
||||
}
|
||||
.checkbox label,
|
||||
.radio label {
|
||||
min-height: 20px;
|
||||
margin-bottom: 0;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
text-align: left;
|
||||
}
|
||||
#kc-content {
|
||||
width: 100%;
|
||||
}
|
||||
#kc-content-wrapper {
|
||||
margin-top: 20px;
|
||||
}
|
||||
#kc-form-options .checkbox {
|
||||
margin-top: 0;
|
||||
color: #72767b;
|
||||
}
|
||||
.bg-image {
|
||||
font-family: "Noto Sans Thai", sans-serif;
|
||||
font-family: "Noto Sans Thai", sans-serif;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-size: cover;
|
||||
height: 100vh;
|
||||
background: rgb(30, 50, 49);
|
||||
background: linear-gradient(
|
||||
0deg,
|
||||
rgba(30, 50, 49, 1) 0%,
|
||||
rgba(20, 120, 99, 1) 100%
|
||||
);
|
||||
}
|
||||
|
||||
#myimage {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
object-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% !important;
|
||||
background: url(@/assets/logo.png) no-repeat center center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.text-logo {
|
||||
text-align: left;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.subtext-logo {
|
||||
color: #fff;
|
||||
font-weight: 200;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.card-pf {
|
||||
padding: 20px;
|
||||
max-width: 400px;
|
||||
min-width: 30%;
|
||||
position: relative;
|
||||
z-index: 10 !important;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.login-pf-page .login-pf-header {
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.login-pf-page .login-pf-header h1 {
|
||||
flex-wrap: wrap;
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
h1#kc-page-title {
|
||||
font-weight: 800;
|
||||
font-size: 1.3rem;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.login-pf-page .login-pf-header h1 {
|
||||
font-size: 16px;
|
||||
}
|
||||
.img_absolute_line {
|
||||
position: absolute;
|
||||
height: auto;
|
||||
width: 400px;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -28,24 +28,6 @@ export default defineConfig({
|
|||
name: 'BMA-Checkin',
|
||||
short_name: 'EHR Checkin',
|
||||
theme_color: '#ffffff',
|
||||
icons: [
|
||||
{
|
||||
src: 'icons/android-chrome-192x192.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: 'icons/android-chrome-512x512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: 'icons/android-chrome-512x512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
purpose: ['any', 'maskable'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
|
|
|||