refactor: rename SKLogin component to PageLogin and update references
This commit is contained in:
@@ -0,0 +1,532 @@
|
||||
<template>
|
||||
<v-sheet class="bg-surface" :class="layoutClass" height="100%">
|
||||
<!-- Side Layouts -->
|
||||
<v-row
|
||||
v-if="props.layout !== 'card'"
|
||||
class="fill-height"
|
||||
:class="{ 'flex-row-reverse': props.layout === 'side-right' }"
|
||||
no-gutters
|
||||
>
|
||||
<!-- Illustration Column -->
|
||||
<v-col
|
||||
class="illustration-panel d-none d-sm-flex align-center justify-center position-relative flex-grow-1"
|
||||
cols="12"
|
||||
lg="8"
|
||||
sm="6"
|
||||
>
|
||||
<div class="brand-header position-absolute top-0 left-0 px-2 pt-4 pa-lg-4">
|
||||
<LoginBrand :title="props.branding.title" />
|
||||
</div>
|
||||
<v-sheet
|
||||
class="board-wrapper pa-2 pa-lg-0"
|
||||
color="rgba(var(--v-theme-surface), 0.8)"
|
||||
elevation="0"
|
||||
max-width="680"
|
||||
rounded="lg"
|
||||
width="100%"
|
||||
>
|
||||
<LoginAnnouncementBoard
|
||||
:all-tab-label="props.announcementBoard.allTabLabel"
|
||||
:date-header="props.announcementBoard.dateHeader"
|
||||
:empty-text="props.announcementBoard.emptyText"
|
||||
:items="props.announcementBoard.items"
|
||||
:items-per-page="props.announcementBoard.itemsPerPage"
|
||||
:pagination-label="props.announcementBoard.paginationLabel"
|
||||
:school-header="props.announcementBoard.schoolHeader"
|
||||
:system-announcements="props.announcementBoard.systemAnnouncements"
|
||||
:tabs="props.announcementBoard.tabs"
|
||||
:title="props.announcementBoard.title"
|
||||
:title-header="props.announcementBoard.titleHeader"
|
||||
@select-announcement="handleSelectAnnouncement"
|
||||
/>
|
||||
</v-sheet>
|
||||
</v-col>
|
||||
|
||||
<v-col
|
||||
class="bg-background d-flex flex-column flex-grow-1 px-4 py-2 py-md-0"
|
||||
cols="12"
|
||||
lg="4"
|
||||
sm="6"
|
||||
>
|
||||
<div v-if="showMobileAnnouncementBanner" class="banner-wrapper px-3">
|
||||
<v-banner
|
||||
class="d-sm-none mb-2"
|
||||
density="comfortable"
|
||||
lines="one"
|
||||
:mobile="false"
|
||||
:stacked="false"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-slide-x-transition appear>
|
||||
<div class="mobile-banner-icon-wrap d-flex align-center">
|
||||
<v-icon
|
||||
class="mobile-banner-icon"
|
||||
color="primary"
|
||||
size="small"
|
||||
:icon="mdiBullhornVariantOutline"
|
||||
/>
|
||||
</div>
|
||||
</v-slide-x-transition>
|
||||
</template>
|
||||
<v-banner-text>{{ mobileAnnouncementBannerText }}</v-banner-text>
|
||||
<template #actions>
|
||||
<v-btn
|
||||
class="text-none"
|
||||
color="primary"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="mobileAnnouncementSheetVisible = true"
|
||||
>
|
||||
{{ props.mobileAnnouncement.viewAllText }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-banner>
|
||||
</div>
|
||||
<LoginToolBar
|
||||
v-if="props.toolbar.show"
|
||||
:locale="props.toolbar.locale"
|
||||
:locales="props.toolbar.locales"
|
||||
@change-locale="handleChangeLocale"
|
||||
@toggle-layout="handleToggleLayout"
|
||||
/>
|
||||
<div
|
||||
class="login-form-wrapper d-flex flex-column justify-center py-0 py-sm-4 py-lg-8 px-4 px-sm-6 px-lg-12 flex-grow-1"
|
||||
>
|
||||
<div class="login-header-height d-flex d-sm-none justify-center align-start">
|
||||
<LoginBrand :title="props.branding.title" />
|
||||
</div>
|
||||
<LoginHeader
|
||||
class="d-none d-sm-block"
|
||||
:welcome-description="props.header.welcomeDescription"
|
||||
:welcome-text="props.header.welcomeText"
|
||||
/>
|
||||
<LoginForm
|
||||
:acc-placeholder="props.form.accPlaceholder"
|
||||
:forgot-password-href="props.form.forgotPassword.href"
|
||||
:forgot-password-target="props.form.forgotPassword.target"
|
||||
:forgot-password-text="props.form.forgotPassword.text"
|
||||
:passw-placeholder="props.form.passwPlaceholder"
|
||||
:remember-me-label="props.form.rememberMeLabel"
|
||||
:remember-storage-key="props.form.rememberStorageKey"
|
||||
:submit-text="props.form.submitText"
|
||||
@forgot-password="handleForgotPassword"
|
||||
@submit="handleLogin"
|
||||
>
|
||||
<template v-if="props.form.withCaptcha" #verify>
|
||||
<LoginVerify
|
||||
:captcha="props.form.captcha"
|
||||
:captcha-placeholder="props.form.captchaPlaceholder"
|
||||
:error-message="props.form.captchaErrorMessage"
|
||||
:loading="props.form.captchaLoading"
|
||||
:model-value="props.form.captchaValue"
|
||||
:refresh-title="props.form.refreshTitle"
|
||||
:verified="props.form.captchaVerified"
|
||||
:verify-text="props.form.verifyText"
|
||||
@refresh="handleCaptchaRefresh"
|
||||
@update:model-value="handleCaptchaChange"
|
||||
/>
|
||||
</template>
|
||||
</LoginForm>
|
||||
<div class="mt-auto py-8 text-center text-caption text-grey-darken-1">
|
||||
Copyright © {{ new Date().getFullYear() }} {{ props.branding.organization }}
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Card Layout (Centered) -->
|
||||
<v-row
|
||||
v-else
|
||||
class="fill-height align-center justify-center bg-background pa-4 pa-md-0"
|
||||
no-gutters
|
||||
>
|
||||
<v-card
|
||||
class="rounded-lg"
|
||||
:class="props.toolbar.show ? 'px-8 pt-0 pb-4' : 'pa-8'"
|
||||
elevation="10"
|
||||
max-width="450"
|
||||
width="100%"
|
||||
>
|
||||
<LoginToolBar
|
||||
v-if="props.toolbar.show"
|
||||
:locale="props.toolbar.locale"
|
||||
:locales="props.toolbar.locales"
|
||||
@change-locale="handleChangeLocale"
|
||||
@toggle-layout="handleToggleLayout"
|
||||
/>
|
||||
<div class="d-flex justify-center mb-6 mb-md-4">
|
||||
<LoginBrand :title="props.branding.title" />
|
||||
</div>
|
||||
<LoginHeader
|
||||
class="d-none d-md-block"
|
||||
:welcome-description="props.header.welcomeDescription"
|
||||
:welcome-text="props.header.welcomeText"
|
||||
/>
|
||||
<LoginForm
|
||||
:acc-placeholder="props.form.accPlaceholder"
|
||||
:forgot-password-href="props.form.forgotPassword.href"
|
||||
:forgot-password-target="props.form.forgotPassword.target"
|
||||
:forgot-password-text="props.form.forgotPassword.text"
|
||||
:passw-placeholder="props.form.passwPlaceholder"
|
||||
:remember-me-label="props.form.rememberMeLabel"
|
||||
:remember-storage-key="props.form.rememberStorageKey"
|
||||
:submit-text="props.form.submitText"
|
||||
@forgot-password="handleForgotPassword"
|
||||
@submit="handleLogin"
|
||||
>
|
||||
<template v-if="props.form.withCaptcha" #verify>
|
||||
<LoginVerify
|
||||
:captcha="props.form.captcha"
|
||||
:captcha-placeholder="props.form.captchaPlaceholder"
|
||||
:error-message="props.form.captchaErrorMessage"
|
||||
:loading="props.form.captchaLoading"
|
||||
:model-value="props.form.captchaValue"
|
||||
:refresh-title="props.form.refreshTitle"
|
||||
:verified="props.form.captchaVerified"
|
||||
:verify-text="props.form.verifyText"
|
||||
@refresh="handleCaptchaRefresh"
|
||||
@update:model-value="handleCaptchaChange"
|
||||
/>
|
||||
</template>
|
||||
</LoginForm>
|
||||
<div class="mt-8 text-center text-caption text-grey-darken-1">
|
||||
Copyright © {{ new Date().getFullYear() }} {{ props.branding.organization }}
|
||||
</div>
|
||||
</v-card>
|
||||
</v-row>
|
||||
|
||||
<v-bottom-sheet v-model="mobileAnnouncementSheetVisible" class="d-sm-none">
|
||||
<v-card rounded="t-xl">
|
||||
<v-card-title class="text-subtitle-1 font-weight-bold">
|
||||
{{ props.mobileAnnouncement.listTitle }}
|
||||
</v-card-title>
|
||||
<v-list lines="two">
|
||||
<v-list-item v-for="item in mobileAnnouncementItems" :key="item.id">
|
||||
<v-list-item-title>{{ item.content }}</v-list-item-title>
|
||||
<v-list-item-subtitle v-if="item.title || item.createdAt">
|
||||
{{ item.title }}<span v-if="item.title && item.createdAt"> ・ </span
|
||||
>{{ item.createdAt }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="mobileAnnouncementItems.length === 0">
|
||||
<v-list-item-title>{{ props.mobileAnnouncement.emptyText }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<v-card-actions class="justify-end">
|
||||
<v-btn color="primary" variant="text" @click="mobileAnnouncementSheetVisible = false">
|
||||
{{ props.mobileAnnouncement.closeText }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-bottom-sheet>
|
||||
</v-sheet>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { mdiBullhornVariantOutline } from '@mdi/js'
|
||||
import { computed, ref } from 'vue'
|
||||
import LoginAnnouncementBoard from './base/login/LoginAnnouncementBoard.vue'
|
||||
import LoginBrand from './base/login/LoginBrand.vue'
|
||||
import LoginForm from './base/login/LoginForm.vue'
|
||||
import LoginHeader from './base/login/LoginHeader.vue'
|
||||
import LoginToolBar from './base/login/LoginToolBar.vue'
|
||||
import LoginVerify from './base/login/LoginVerify.vue'
|
||||
|
||||
interface BrandingConfig {
|
||||
title?: string
|
||||
organization?: string
|
||||
}
|
||||
|
||||
interface IllustrationConfig {
|
||||
image?: string | null
|
||||
title?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
interface HeaderConfig {
|
||||
welcomeText?: string
|
||||
welcomeDescription?: string
|
||||
}
|
||||
|
||||
interface AnnouncementTabConfig {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
interface AnnouncementItemConfig {
|
||||
id: string | number
|
||||
date: string
|
||||
school: string
|
||||
title: string
|
||||
tab?: string
|
||||
}
|
||||
|
||||
interface AnnouncementBoardConfig {
|
||||
title?: string
|
||||
tabs?: AnnouncementTabConfig[]
|
||||
items?: AnnouncementItemConfig[]
|
||||
systemAnnouncements?: {
|
||||
id: string | number
|
||||
content: string
|
||||
title?: string
|
||||
createdAt?: string
|
||||
}[]
|
||||
allTabLabel?: string
|
||||
itemsPerPage?: number
|
||||
dateHeader?: string
|
||||
schoolHeader?: string
|
||||
titleHeader?: string
|
||||
emptyText?: string
|
||||
paginationLabel?: string
|
||||
}
|
||||
|
||||
interface MobileAnnouncementConfig {
|
||||
items?: {
|
||||
id: string | number
|
||||
content: string
|
||||
title?: string
|
||||
createdAt?: string
|
||||
}[]
|
||||
show?: boolean
|
||||
viewAllText?: string
|
||||
listTitle?: string
|
||||
closeText?: string
|
||||
emptyText?: string
|
||||
}
|
||||
|
||||
interface ForgotPasswordConfig {
|
||||
text?: string
|
||||
href?: string
|
||||
target?: string
|
||||
}
|
||||
|
||||
interface FormConfig {
|
||||
accPlaceholder?: string
|
||||
passwPlaceholder?: string
|
||||
rememberMeLabel?: string
|
||||
submitText?: string
|
||||
rememberStorageKey?: string
|
||||
withCaptcha?: boolean
|
||||
captcha?: {
|
||||
imgUrl?: string
|
||||
id?: string
|
||||
tokenValue?: string
|
||||
}
|
||||
captchaValue?: string
|
||||
captchaLoading?: boolean
|
||||
captchaErrorMessage?: string
|
||||
captchaVerified?: boolean
|
||||
verifyText?: string
|
||||
captchaPlaceholder?: string
|
||||
refreshTitle?: string
|
||||
forgotPassword: ForgotPasswordConfig
|
||||
}
|
||||
|
||||
interface ToolBarConfig {
|
||||
show?: boolean
|
||||
locale?: string
|
||||
locales?: string[]
|
||||
}
|
||||
|
||||
interface Props {
|
||||
layout: 'side-left' | 'side-right' | 'card'
|
||||
branding: BrandingConfig
|
||||
illustration: IllustrationConfig
|
||||
announcementBoard: AnnouncementBoardConfig
|
||||
mobileAnnouncement: MobileAnnouncementConfig
|
||||
header: HeaderConfig
|
||||
form: FormConfig
|
||||
toolbar: ToolBarConfig
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
layout: 'side-left',
|
||||
branding: () => ({
|
||||
title: 'Skyteck Login',
|
||||
organization: 'school',
|
||||
}),
|
||||
illustration: () => ({
|
||||
image: null,
|
||||
title: 'Login',
|
||||
description: 'Login to your account',
|
||||
}),
|
||||
announcementBoard: () => ({
|
||||
title: '學校公告區',
|
||||
tabs: [
|
||||
{ label: '全部', value: '__all__' },
|
||||
{ label: '國中', value: 'junior' },
|
||||
{ label: '高中', value: 'senior' },
|
||||
],
|
||||
items: [
|
||||
{
|
||||
id: 'announcement-1',
|
||||
date: '2024-03-19',
|
||||
school: '市立實踐國中',
|
||||
title: '臺北市立實踐國中徵求113學年度教學支援工作人員',
|
||||
tab: 'junior',
|
||||
},
|
||||
],
|
||||
systemAnnouncements: [],
|
||||
allTabLabel: '全部',
|
||||
itemsPerPage: 5,
|
||||
dateHeader: '公告時間',
|
||||
schoolHeader: '公告學校',
|
||||
titleHeader: '公告標題',
|
||||
emptyText: '目前沒有公告資料',
|
||||
paginationLabel: '總筆數:',
|
||||
}),
|
||||
mobileAnnouncement: () => ({
|
||||
items: [],
|
||||
show: false,
|
||||
viewAllText: '查看全部',
|
||||
listTitle: '系統公告',
|
||||
closeText: '關閉',
|
||||
emptyText: '目前沒有公告',
|
||||
}),
|
||||
header: () => ({
|
||||
welcomeText: 'Welcome back 👋🏻',
|
||||
welcomeDescription: 'Please enter your account password to login',
|
||||
}),
|
||||
form: () => ({
|
||||
accPlaceholder: '請輸入帳號',
|
||||
passwPlaceholder: '請輸入密碼',
|
||||
rememberMeLabel: '記住帳號',
|
||||
submitText: '登入',
|
||||
rememberStorageKey: 'sklogin.remember.username',
|
||||
withCaptcha: true,
|
||||
captcha: undefined,
|
||||
captchaValue: '',
|
||||
captchaLoading: false,
|
||||
captchaErrorMessage: '',
|
||||
captchaVerified: false,
|
||||
verifyText: '驗證',
|
||||
captchaPlaceholder: '驗證碼',
|
||||
refreshTitle: '點擊刷新驗證碼',
|
||||
forgotPassword: {
|
||||
text: '忘記密碼?',
|
||||
href: '',
|
||||
target: undefined,
|
||||
},
|
||||
}),
|
||||
toolbar: () => ({
|
||||
show: true,
|
||||
locale: 'zh-TW',
|
||||
locales: ['zh-TW', 'en-US'],
|
||||
}),
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'submit',
|
||||
'change-locale',
|
||||
'forgot-password',
|
||||
'captcha-refresh',
|
||||
'captcha-change',
|
||||
'toggle-layout',
|
||||
'select-announcement',
|
||||
])
|
||||
|
||||
const mobileAnnouncementSheetVisible = ref(false)
|
||||
|
||||
const mobileAnnouncementItems = computed(() => props.mobileAnnouncement.items ?? [])
|
||||
|
||||
const showMobileAnnouncementBanner = computed(() => {
|
||||
if (props.mobileAnnouncement.show === false) return false
|
||||
return mobileAnnouncementItems.value.length > 0
|
||||
})
|
||||
|
||||
const mobileAnnouncementBannerText = computed(() => {
|
||||
return mobileAnnouncementItems.value[0]?.content ?? ''
|
||||
})
|
||||
|
||||
const layoutClass = computed(() => {
|
||||
return `layout-${props.layout}`
|
||||
})
|
||||
|
||||
function handleLogin(formData: Record<string, unknown>) {
|
||||
emit('submit', formData)
|
||||
}
|
||||
|
||||
function handleCaptchaRefresh() {
|
||||
emit('captcha-refresh')
|
||||
}
|
||||
|
||||
function handleCaptchaChange(value: string) {
|
||||
emit('captcha-change', value)
|
||||
}
|
||||
|
||||
function handleChangeLocale(nextLocale: string) {
|
||||
emit('change-locale', nextLocale)
|
||||
}
|
||||
|
||||
function handleToggleLayout() {
|
||||
emit('toggle-layout')
|
||||
}
|
||||
|
||||
function handleForgotPassword(e: MouseEvent) {
|
||||
emit('forgot-password', e)
|
||||
}
|
||||
|
||||
function handleSelectAnnouncement(item: AnnouncementItemConfig) {
|
||||
emit('select-announcement', item)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.v-banner__prepend) {
|
||||
align-self: center;
|
||||
margin-inline-end: 16px;
|
||||
}
|
||||
|
||||
:deep(.v-banner-actions) {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.mobile-banner-icon {
|
||||
animation: mobile-banner-breathe 2.8s ease-in-out infinite;
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
@keyframes mobile-banner-breathe {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.9;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1.08);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.mobile-banner-icon {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
.illustration-panel {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgb(var(--v-theme-background)) 0%,
|
||||
rgb(var(--v-theme-surface)) 100%
|
||||
);
|
||||
border-right: 1px solid rgba(var(--v-theme-on-surface), 0.05);
|
||||
}
|
||||
|
||||
.login-form-wrapper {
|
||||
max-width: 450px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-header-height {
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
/* Specific styles for side-right to flip border */
|
||||
.layout-side-right .illustration-panel {
|
||||
border-right: none;
|
||||
border-left: 1px solid rgba(var(--v-theme-on-surface), 0.05);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user