feat: add SingleRecordMnt component for student record maintenance with search, add, edit, view, and delete functionalities
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
<template>
|
||||
<SKLogin
|
||||
:announcement-board="announcementBoard"
|
||||
:branding="branding"
|
||||
:form="form"
|
||||
:header="header"
|
||||
:illustration="illustration"
|
||||
:layout="formPositionLayout" :mobile-announcement="mobileAnnouncement" :toolbar="toolbar"
|
||||
@captcha-change="handleCaptchaChange" @captcha-refresh="handleCaptchaRefresh" @change-locale="handleChangeLocale"
|
||||
@forgot-password="handleForgotPassword" @select-announcement="handleSelectAnnouncement" @submit="onLogin"
|
||||
@toggle-layout="handleToggleLayout" />
|
||||
|
||||
<v-dialog v-model="dialogVisible" width="360">
|
||||
<v-card>
|
||||
<v-card-title>{{ dialogTitle }}</v-card-title>
|
||||
<v-card-text>{{ dialogMessage }}</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn color="primary" variant="flat" @click="dialogVisible = false">
|
||||
{{ t('common.ok') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog v-model="announcementDialogVisible" max-width="720">
|
||||
<v-card>
|
||||
<v-card-title class="text-h6">
|
||||
{{ selectedAnnouncement?.title }}
|
||||
</v-card-title>
|
||||
<v-card-subtitle class="pt-2">
|
||||
{{ selectedAnnouncement?.date }} ・ {{ selectedAnnouncement?.school }}
|
||||
</v-card-subtitle>
|
||||
<v-card-text class="text-body-1">
|
||||
{{ selectedAnnouncementDetail }}
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn color="primary" variant="flat" @click="announcementDialogVisible = false">
|
||||
關閉
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import HyakkaouAcademyImage from '@/assets/logo.png'
|
||||
import SKLogin from '@/components/SKLogin.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { type LoginAnnouncementListItem, useLoginAnnouncementsStore } from '@/stores/loginAnnouncements'
|
||||
import { useMenuStore } from '@/stores/menu'
|
||||
import { useSnackbarStore } from '@/stores/snackbar'
|
||||
|
||||
type LayoutType = 'side-left' | 'side-right' | 'card'
|
||||
|
||||
// i18n
|
||||
const { t, locale } = useI18n()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const authStore = useAuthStore()
|
||||
const loginAnnouncementsStore = useLoginAnnouncementsStore()
|
||||
const menuStore = useMenuStore()
|
||||
const snackbarStore = useSnackbarStore()
|
||||
const {
|
||||
boardConfig: announcementBoard,
|
||||
mobileAnnouncementConfig: mobileAnnouncement,
|
||||
selectedAnnouncement,
|
||||
selectedAnnouncementDetail,
|
||||
} =
|
||||
storeToRefs(loginAnnouncementsStore)
|
||||
|
||||
// 語系選項
|
||||
const locales = ['zh-TW', 'en-US']
|
||||
|
||||
// 插圖圖片來源
|
||||
const illustrationImage = ref(HyakkaouAcademyImage)
|
||||
|
||||
// 功能開關與版型
|
||||
const formPositionLayout = ref<LayoutType>('side-left')
|
||||
|
||||
// 功能開關:是否啟用驗證碼
|
||||
const withCaptcha = ref(true)
|
||||
|
||||
// 文字內容(i18n)
|
||||
const title = computed(() => t('pages.login.title'))
|
||||
const organization = computed(() => t('pages.login.organization'))
|
||||
const accPlaceholder = computed(() => t('pages.login.accPlaceholder'))
|
||||
const passwPlaceholder = computed(() => t('pages.login.passwPlaceholder'))
|
||||
const illustrationTitle = computed(() => t('pages.login.illustrationTitle'))
|
||||
const illustrationDescription = computed(() => t('pages.login.illustrationDescription'))
|
||||
const welcomeText = computed(() => t('pages.login.welcomeText'))
|
||||
const welcomeDescription = computed(() => t('pages.login.welcomeDescription'))
|
||||
const rememberMeLabel = computed(() => t('pages.login.rememberMeLabel'))
|
||||
const forgotPasswordText = computed(() => t('pages.login.forgotPasswordText'))
|
||||
const submitText = computed(() => t('pages.login.submitText'))
|
||||
const verifyText = computed(() => t('pages.login.verifyText'))
|
||||
const captchaPlaceholder = computed(() => t('pages.login.captchaPlaceholder'))
|
||||
const refreshTitle = computed(() => t('pages.login.refreshTitle'))
|
||||
|
||||
// 連結與儲存設定
|
||||
// 忘記密碼連結(由 form.forgotPassword 設定)
|
||||
const forgotPasswordHref = ref('/forgot-password')
|
||||
const forgotPasswordTarget = ref<string | undefined>(undefined)
|
||||
// 記住帳號的 localStorage key
|
||||
const rememberStorageKey = ref('playground.login.remember.username')
|
||||
// 驗證碼 API
|
||||
const captchaValue = ref('')
|
||||
|
||||
// 驗證與對話框狀態
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const dialogMessage = ref('')
|
||||
const announcementDialogVisible = ref(false)
|
||||
|
||||
// 內容組合(傳入 SKLogin)
|
||||
const branding = computed(() => ({
|
||||
title: title.value,
|
||||
organization: organization.value,
|
||||
}))
|
||||
|
||||
const illustration = computed(() => ({
|
||||
image: illustrationImage.value,
|
||||
title: illustrationTitle.value,
|
||||
description: illustrationDescription.value,
|
||||
}))
|
||||
|
||||
const header = computed(() => ({
|
||||
welcomeText: welcomeText.value,
|
||||
welcomeDescription: welcomeDescription.value,
|
||||
}))
|
||||
|
||||
// 表單區塊設定(含功能開關)
|
||||
const form = computed(() => ({
|
||||
accPlaceholder: accPlaceholder.value,
|
||||
passwPlaceholder: passwPlaceholder.value,
|
||||
rememberMeLabel: rememberMeLabel.value,
|
||||
submitText: submitText.value,
|
||||
verifyText: verifyText.value,
|
||||
captchaPlaceholder: captchaPlaceholder.value,
|
||||
refreshTitle: refreshTitle.value,
|
||||
rememberStorageKey: rememberStorageKey.value,
|
||||
// 功能開關:是否顯示驗證碼
|
||||
withCaptcha: withCaptcha.value,
|
||||
captcha: authStore.captcha
|
||||
? {
|
||||
imgUrl: authStore.captcha.dntCaptchaImgUrl,
|
||||
id: authStore.captcha.dntCaptchaId,
|
||||
tokenValue: authStore.captcha.dntCaptchaTokenValue,
|
||||
}
|
||||
: undefined,
|
||||
captchaValue: captchaValue.value,
|
||||
captchaLoading: authStore.captchaLoading,
|
||||
captchaErrorMessage: authStore.captchaErrorMessage ?? '',
|
||||
captchaVerified: false,
|
||||
forgotPassword: {
|
||||
text: forgotPasswordText.value,
|
||||
href: forgotPasswordHref.value,
|
||||
target: forgotPasswordTarget.value,
|
||||
},
|
||||
}))
|
||||
|
||||
|
||||
// 右上工具列設定(含顯示開關)
|
||||
const toolbar = computed(() => ({
|
||||
// 功能開關:是否顯示語系切換工具列
|
||||
show: true,
|
||||
locale: locale.value,
|
||||
locales,
|
||||
}))
|
||||
|
||||
// 事件處理
|
||||
function handleForgotPassword (e: MouseEvent) {
|
||||
console.log('Playground Forgot Password Click:', e)
|
||||
}
|
||||
|
||||
function handleChangeLocale (nextLocale: string) {
|
||||
locale.value = nextLocale
|
||||
localStorage.setItem('playground.locale', nextLocale)
|
||||
}
|
||||
|
||||
async function handleCaptchaRefresh () {
|
||||
captchaValue.value = ''
|
||||
await authStore.getCaptcha()
|
||||
}
|
||||
|
||||
function handleCaptchaChange (value: string) {
|
||||
captchaValue.value = value
|
||||
}
|
||||
|
||||
function handleToggleLayout () {
|
||||
const layoutOrder: LayoutType[] = ['side-left', 'side-right', 'card']
|
||||
const currentIndex = layoutOrder.indexOf(formPositionLayout.value)
|
||||
const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % layoutOrder.length
|
||||
formPositionLayout.value = layoutOrder[nextIndex] ?? 'side-left'
|
||||
}
|
||||
|
||||
function handleSelectAnnouncement (item: LoginAnnouncementListItem) {
|
||||
loginAnnouncementsStore.selectById(item.id)
|
||||
announcementDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function onLogin (data: Record<string, unknown>) {
|
||||
if (withCaptcha.value && !captchaValue.value) {
|
||||
dialogTitle.value = t('common.notice')
|
||||
dialogMessage.value = t('pages.login.alert.verifyRequired')
|
||||
dialogVisible.value = true
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
dialogTitle.value = t('common.notice')
|
||||
|
||||
const inputUserId = String(data.username ?? '').trim()
|
||||
const inputPassword = String(data.password ?? '').trim()
|
||||
const isDev = import.meta.env.DEV
|
||||
const devDefaultUserId = String(import.meta.env.VITE_DEV_DEFAULT_USER_ID ?? '').trim()
|
||||
const devDefaultPassword = String(import.meta.env.VITE_DEV_DEFAULT_PASSWORD ?? '').trim()
|
||||
const userId = isDev && !inputUserId ? devDefaultUserId : inputUserId
|
||||
const password = isDev && !inputPassword ? devDefaultPassword : inputPassword
|
||||
|
||||
await authStore.login({
|
||||
UserID: userId,
|
||||
Password: password,
|
||||
DNTCaptchaInputText: captchaValue.value,
|
||||
})
|
||||
|
||||
menuStore.getMenu(authStore.user?.id ?? '')
|
||||
|
||||
// menuStore.getFavorite(authStore.user?.id ?? '')
|
||||
|
||||
snackbarStore.show({
|
||||
message: t('pages.login.alert.loginSuccess'),
|
||||
color: 'success',
|
||||
timeout: 2000,
|
||||
location: 'top right',
|
||||
variant: 'flat',
|
||||
})
|
||||
|
||||
const redirect = (route.query.redirect as string) || '/'
|
||||
await router.push(redirect.startsWith('/') ? redirect : '/')
|
||||
} catch (error) {
|
||||
console.error('Login error:', error)
|
||||
dialogTitle.value = t('common.notice')
|
||||
dialogMessage.value = t('pages.login.alert.loginFailed')
|
||||
dialogVisible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loginAnnouncementsStore.hydrate()
|
||||
loginAnnouncementsStore.fetchMobileAnnouncements()
|
||||
authStore.getCaptcha()
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user