Compare commits

..

4 Commits

Author SHA1 Message Date
skytek_xinliang f3eb9782c6 feat: 記住帳號, 忘記密碼開關 2026-05-22 10:43:17 +08:00
skytek_xinliang 8378c44ad7 feat: 公告開關 2026-05-22 10:30:04 +08:00
skytek_xinliang 8cf5aacf21 fix: captcha 開關 2026-05-22 09:51:11 +08:00
skytek_xinliang 59d04a4d7e fix: 環境變數讀取 2026-05-22 09:50:54 +08:00
9 changed files with 228 additions and 114 deletions
+13
View File
@@ -1,3 +1,16 @@
# vite / vite dev:預設 mode = development
# vite build:預設 mode = production
# vite --mode staging:改成 staging
# vite build --mode developmentbuild 但用 development mode
# 覆蓋優先從低至高
# .env
# .env.local
# .env.[mode]
# .env.[mode].local
# Vite dev proxy 目標後端 URL。 # Vite dev proxy 目標後端 URL。
VITE_PROXY_TARGET=http://192.168.89.54:9002 VITE_PROXY_TARGET=http://192.168.89.54:9002
+17 -1
View File
@@ -18,6 +18,7 @@
<LoginBrand :title="props.branding.title" /> <LoginBrand :title="props.branding.title" />
</div> </div>
<v-sheet <v-sheet
v-if="props.withAnnouncement"
class="board-wrapper pa-2 pa-lg-0" class="board-wrapper pa-2 pa-lg-0"
color="rgba(var(--v-theme-surface), 0.8)" color="rgba(var(--v-theme-surface), 0.8)"
elevation="0" elevation="0"
@@ -109,6 +110,8 @@
:remember-me-label="props.form.rememberMeLabel" :remember-me-label="props.form.rememberMeLabel"
:remember-storage-key="props.form.rememberStorageKey" :remember-storage-key="props.form.rememberStorageKey"
:submit-text="props.form.submitText" :submit-text="props.form.submitText"
:with-forgot-password="props.form.withForgotPassword"
:with-remember-account="props.form.withRememberAccount"
@forgot-password="handleForgotPassword" @forgot-password="handleForgotPassword"
@submit="handleLogin" @submit="handleLogin"
> >
@@ -171,6 +174,8 @@
:remember-me-label="props.form.rememberMeLabel" :remember-me-label="props.form.rememberMeLabel"
:remember-storage-key="props.form.rememberStorageKey" :remember-storage-key="props.form.rememberStorageKey"
:submit-text="props.form.submitText" :submit-text="props.form.submitText"
:with-forgot-password="props.form.withForgotPassword"
:with-remember-account="props.form.withRememberAccount"
@forgot-password="handleForgotPassword" @forgot-password="handleForgotPassword"
@submit="handleLogin" @submit="handleLogin"
> >
@@ -195,7 +200,11 @@
</v-card> </v-card>
</v-row> </v-row>
<v-bottom-sheet v-model="mobileAnnouncementSheetVisible" class="d-sm-none"> <v-bottom-sheet
v-if="props.withAnnouncement"
v-model="mobileAnnouncementSheetVisible"
class="d-sm-none"
>
<v-card rounded="t-xl"> <v-card rounded="t-xl">
<v-card-title class="text-subtitle-1 font-weight-bold"> <v-card-title class="text-subtitle-1 font-weight-bold">
{{ props.mobileAnnouncement.listTitle }} {{ props.mobileAnnouncement.listTitle }}
@@ -306,6 +315,8 @@ interface FormConfig {
rememberMeLabel?: string rememberMeLabel?: string
submitText?: string submitText?: string
rememberStorageKey?: string rememberStorageKey?: string
withForgotPassword?: boolean
withRememberAccount?: boolean
withCaptcha?: boolean withCaptcha?: boolean
captcha?: { captcha?: {
imgUrl?: string imgUrl?: string
@@ -330,6 +341,7 @@ interface ToolBarConfig {
interface Props { interface Props {
layout: 'side-left' | 'side-right' | 'card' layout: 'side-left' | 'side-right' | 'card'
withAnnouncement?: boolean
branding: BrandingConfig branding: BrandingConfig
illustration: IllustrationConfig illustration: IllustrationConfig
announcementBoard: AnnouncementBoardConfig announcementBoard: AnnouncementBoardConfig
@@ -341,6 +353,7 @@ interface Props {
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
layout: 'side-left', layout: 'side-left',
withAnnouncement: true,
branding: () => ({ branding: () => ({
title: 'Skyteck Login', title: 'Skyteck Login',
organization: 'school', organization: 'school',
@@ -393,6 +406,8 @@ const props = withDefaults(defineProps<Props>(), {
rememberMeLabel: '記住帳號', rememberMeLabel: '記住帳號',
submitText: '登入', submitText: '登入',
rememberStorageKey: 'sklogin.remember.username', rememberStorageKey: 'sklogin.remember.username',
withForgotPassword: true,
withRememberAccount: true,
withCaptcha: true, withCaptcha: true,
captcha: undefined, captcha: undefined,
captchaValue: '', captchaValue: '',
@@ -430,6 +445,7 @@ const mobileAnnouncementSheetVisible = ref(false)
const mobileAnnouncementItems = computed(() => props.mobileAnnouncement.items ?? []) const mobileAnnouncementItems = computed(() => props.mobileAnnouncement.items ?? [])
const showMobileAnnouncementBanner = computed(() => { const showMobileAnnouncementBanner = computed(() => {
if (!props.withAnnouncement) return false
if (props.mobileAnnouncement.show === false) return false if (props.mobileAnnouncement.show === false) return false
return mobileAnnouncementItems.value.length > 0 return mobileAnnouncementItems.value.length > 0
}) })
+29 -2
View File
@@ -1,5 +1,5 @@
<template> <template>
<v-form @submit.prevent="$emit('submit', { username, password, rememberMe })"> <v-form @submit.prevent="handleSubmit">
<v-text-field <v-text-field
v-model="username" v-model="username"
bg-color="surface" bg-color="surface"
@@ -27,8 +27,12 @@
<slot name="verify"></slot> <slot name="verify"></slot>
<div class="d-flex align-center justify-space-between mb-6 mb-md-4"> <div
v-if="props.withRememberAccount || props.withForgotPassword"
class="d-flex align-center justify-space-between mb-6 mb-md-4"
>
<v-checkbox <v-checkbox
v-if="props.withRememberAccount"
v-model="rememberMe" v-model="rememberMe"
color="primary" color="primary"
density="compact" density="compact"
@@ -36,6 +40,7 @@
:label="props.rememberMeLabel" :label="props.rememberMeLabel"
></v-checkbox> ></v-checkbox>
<a <a
v-if="props.withForgotPassword"
class="text-body-2 text-primary text-decoration-none" class="text-body-2 text-primary text-decoration-none"
:href="props.forgotPasswordHref || '#'" :href="props.forgotPasswordHref || '#'"
:target="props.forgotPasswordTarget" :target="props.forgotPasswordTarget"
@@ -100,11 +105,21 @@ const props = defineProps({
type: String, type: String,
default: 'sklogin.remember.username', default: 'sklogin.remember.username',
}, },
withRememberAccount: {
type: Boolean,
default: true,
},
withForgotPassword: {
type: Boolean,
default: true,
},
}) })
const emit = defineEmits(['submit', 'forgot-password']) const emit = defineEmits(['submit', 'forgot-password'])
onMounted(() => { onMounted(() => {
if (!props.withRememberAccount) return
const saved = localStorage.getItem(props.rememberStorageKey) const saved = localStorage.getItem(props.rememberStorageKey)
if (saved) { if (saved) {
username.value = saved username.value = saved
@@ -113,6 +128,8 @@ onMounted(() => {
}) })
watch([rememberMe, username], ([nextRemember, nextUsername]) => { watch([rememberMe, username], ([nextRemember, nextUsername]) => {
if (!props.withRememberAccount) return
if (!nextRemember) { if (!nextRemember) {
localStorage.removeItem(props.rememberStorageKey) localStorage.removeItem(props.rememberStorageKey)
return return
@@ -126,7 +143,17 @@ watch([rememberMe, username], ([nextRemember, nextUsername]) => {
localStorage.setItem(props.rememberStorageKey, nextUsername) localStorage.setItem(props.rememberStorageKey, nextUsername)
}) })
function handleSubmit() {
emit('submit', {
username: username.value,
password: password.value,
rememberMe: props.withRememberAccount ? rememberMe.value : false,
})
}
function handleForgotPasswordClick(e: MouseEvent) { function handleForgotPasswordClick(e: MouseEvent) {
if (!props.withForgotPassword) return
emit('forgot-password', e) emit('forgot-password', e)
if (!props.forgotPasswordHref) { if (!props.forgotPasswordHref) {
e.preventDefault() e.preventDefault()
@@ -1,5 +1,4 @@
import { defineStore } from 'pinia' import { computed, ref, toValue, watch, type MaybeRefOrGetter } from 'vue'
import { computed, ref, watch } from 'vue'
export interface LoginAnnouncementItem { export interface LoginAnnouncementItem {
id: string | number id: string | number
@@ -25,6 +24,10 @@ export interface LoginMobileAnnouncementItem {
createdAt?: string createdAt?: string
} }
interface UseLoginAnnouncementsOptions {
enabled: MaybeRefOrGetter<boolean>
}
const storageKey = 'sk_playground_login_announcements' const storageKey = 'sk_playground_login_announcements'
const defaultItems: LoginAnnouncementItem[] = [ const defaultItems: LoginAnnouncementItem[] = [
@@ -110,10 +113,11 @@ async function mockFetchMobileAnnouncementsApi(): Promise<LoginMobileAnnouncemen
] ]
} }
export const useLoginAnnouncementsStore = defineStore('loginAnnouncements', () => { export function useLoginAnnouncements(options: UseLoginAnnouncementsOptions) {
const items = ref<LoginAnnouncementItem[]>(readItems()) const items = ref<LoginAnnouncementItem[]>([])
const selectedId = ref<string | number | null>(null) const selectedId = ref<string | number | null>(null)
const mobileAnnouncements = ref<LoginMobileAnnouncementItem[]>([]) const mobileAnnouncements = ref<LoginMobileAnnouncementItem[]>([])
const enabled = computed(() => toValue(options.enabled))
const listItems = computed<LoginAnnouncementListItem[]>(() => const listItems = computed<LoginAnnouncementListItem[]>(() =>
items.value.map((item) => ({ items.value.map((item) => ({
@@ -132,8 +136,8 @@ export const useLoginAnnouncementsStore = defineStore('loginAnnouncements', () =
{ label: '國中', value: 'junior' }, { label: '國中', value: 'junior' },
{ label: '高中', value: 'senior' }, { label: '高中', value: 'senior' },
], ],
items: listItems.value, items: enabled.value ? listItems.value : [],
systemAnnouncements: mobileAnnouncements.value, systemAnnouncements: enabled.value ? mobileAnnouncements.value : [],
itemsPerPage: 5, itemsPerPage: 5,
dateHeader: '公告時間', dateHeader: '公告時間',
schoolHeader: '公告學校', schoolHeader: '公告學校',
@@ -142,7 +146,7 @@ export const useLoginAnnouncementsStore = defineStore('loginAnnouncements', () =
})) }))
const selectedAnnouncement = computed(() => { const selectedAnnouncement = computed(() => {
if (selectedId.value === null) return null if (!enabled.value || selectedId.value === null) return null
return items.value.find((item) => item.id === selectedId.value) ?? null return items.value.find((item) => item.id === selectedId.value) ?? null
}) })
@@ -151,59 +155,54 @@ export const useLoginAnnouncementsStore = defineStore('loginAnnouncements', () =
}) })
const mobileAnnouncementConfig = computed(() => ({ const mobileAnnouncementConfig = computed(() => ({
items: mobileAnnouncements.value, items: enabled.value ? mobileAnnouncements.value : [],
show: mobileAnnouncements.value.length > 0, show: enabled.value && mobileAnnouncements.value.length > 0,
viewAllText: '查看全部', viewAllText: '查看全部',
listTitle: '系統公告', listTitle: '系統公告',
closeText: '關閉', closeText: '關閉',
emptyText: '目前沒有公告', emptyText: '目前沒有公告',
})) }))
const hydrate = () => { function hydrate() {
if (!enabled.value) return
items.value = readItems() items.value = readItems()
} }
const replaceAll = (nextItems: LoginAnnouncementItem[]) => { async function fetchMobileAnnouncements() {
items.value = Array.isArray(nextItems) ? nextItems : [] if (!enabled.value) return
}
const selectById = (id: string | number) => {
selectedId.value = id
}
const clearSelection = () => {
selectedId.value = null
}
const fetchMobileAnnouncements = async () => {
const result = await mockFetchMobileAnnouncementsApi() const result = await mockFetchMobileAnnouncementsApi()
mobileAnnouncements.value = Array.isArray(result) ? result : [] mobileAnnouncements.value = Array.isArray(result) ? result : []
} }
const fetchMobileAnnouncement = async () => { async function load() {
hydrate()
await fetchMobileAnnouncements() await fetchMobileAnnouncements()
} }
function selectById(id: string | number) {
if (!enabled.value) return
selectedId.value = id
}
watch( watch(
items, items,
(val) => { (val) => {
if (!enabled.value) return
writeItems(val) writeItems(val)
}, },
{ deep: true } { deep: true }
) )
return { return {
items,
listItems,
boardConfig, boardConfig,
mobileAnnouncementConfig, mobileAnnouncementConfig,
selectedAnnouncement, selectedAnnouncement,
selectedAnnouncementDetail, selectedAnnouncementDetail,
hydrate, load,
replaceAll,
selectById, selectById,
clearSelection,
fetchMobileAnnouncements,
fetchMobileAnnouncement,
} }
}) }
+84
View File
@@ -0,0 +1,84 @@
import type { CaptchaResponse } from '@/types/api'
import { computed, ref, toValue, type MaybeRefOrGetter } from 'vue'
import { normalizeError } from '@/services/error'
import { authApi } from '@/services/modules/auth'
interface UseLoginCaptchaOptions {
enabled: MaybeRefOrGetter<boolean>
}
export function useLoginCaptcha(options: UseLoginCaptchaOptions) {
const captcha = ref<CaptchaResponse | null>(null)
const captchaValue = ref('')
const captchaLoading = ref(false)
const captchaErrorMessage = ref<string | null>(null)
const enabled = computed(() => toValue(options.enabled))
const formCaptcha = computed(() => {
if (!enabled.value || !captcha.value) return undefined
return {
imgUrl: captcha.value.dntCaptchaImgUrl,
id: captcha.value.dntCaptchaId,
tokenValue: captcha.value.dntCaptchaTokenValue,
}
})
async function loadCaptcha() {
if (!enabled.value) return null
captchaLoading.value = true
captchaErrorMessage.value = null
try {
const { data } = await authApi.getCaptcha()
captcha.value = data
return data
} catch (error_) {
const normalizedError = normalizeError(error_)
captcha.value = null
captchaErrorMessage.value = normalizedError.message
throw normalizedError
} finally {
captchaLoading.value = false
}
}
async function refreshCaptcha() {
if (!enabled.value) return null
captchaValue.value = ''
return await loadCaptcha()
}
function setCaptchaValue(value: string) {
if (!enabled.value) return
captchaValue.value = value
}
function getLoginCaptchaPayload() {
if (!enabled.value) return undefined
if (!captcha.value?.dntCaptchaTextValue || !captcha.value?.dntCaptchaTokenValue) {
throw new Error('驗證碼資料缺失,請先刷新驗證碼')
}
return {
DNTCaptchaInputText: captchaValue.value,
DNTCaptchaText: captcha.value.dntCaptchaTextValue,
DNTCaptchaToken: captcha.value.dntCaptchaTokenValue,
}
}
return {
captchaValue,
captchaLoading,
captchaErrorMessage,
formCaptcha,
loadCaptcha,
refreshCaptcha,
setCaptchaValue,
getLoginCaptchaPayload,
}
}
+9 -42
View File
@@ -1,4 +1,4 @@
import type { CaptchaResponse, LoginPayload, User } from '@/types/api' import type { LoginPayload, User } from '@/types/api'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { normalizeError } from '@/services/error' import { normalizeError } from '@/services/error'
@@ -16,32 +16,12 @@ export const useAuthStore = defineStore('auth', () => {
const token = tokenService.token const token = tokenService.token
const loading = ref(false) const loading = ref(false)
const error = ref<string | null>(null) const error = ref<string | null>(null)
const captcha = ref<CaptchaResponse | null>(null)
const captchaLoading = ref(false)
const captchaErrorMessage = ref<string | null>(null)
// 只針對 login 取消重複請求,避免競態與重複提交 // 只針對 login 取消重複請求,避免競態與重複提交
const loginController = ref<AbortController | null>(null) const loginController = ref<AbortController | null>(null)
const isAuthenticated = computed(() => !!token.value) const isAuthenticated = computed(() => !!token.value)
const roles = computed(() => (user.value?.role ? [user.value.role] : [])) const roles = computed(() => (user.value?.role ? [user.value.role] : []))
const getCaptcha = async () => {
captchaLoading.value = true
captchaErrorMessage.value = null
try {
const { data } = await authApi.getCaptcha()
captcha.value = data
return data
} catch (error_) {
const normalizedError = normalizeError(error_)
captcha.value = null
captchaErrorMessage.value = normalizedError.message
throw normalizedError
} finally {
captchaLoading.value = false
}
}
const login = async (payload: LoginPayload) => { const login = async (payload: LoginPayload) => {
loginController.value?.abort() loginController.value?.abort()
loginController.value = new AbortController() loginController.value = new AbortController()
@@ -49,24 +29,15 @@ export const useAuthStore = defineStore('auth', () => {
error.value = null error.value = null
try { try {
if (!captcha.value?.dntCaptchaTextValue || !captcha.value?.dntCaptchaTokenValue) {
throw new Error('驗證碼資料缺失,請先刷新驗證碼')
}
const requestPayload = {
UserID: payload.UserID,
Password: payload.Password,
DNTCaptchaInputText: payload.DNTCaptchaInputText,
DNTCaptchaText: captcha.value.dntCaptchaTextValue,
DNTCaptchaToken: captcha.value.dntCaptchaTokenValue,
}
const formData = new FormData() const formData = new FormData()
formData.append('UserID', requestPayload.UserID) formData.append('UserID', payload.UserID)
formData.append('Password', requestPayload.Password) formData.append('Password', payload.Password)
formData.append('DNTCaptchaInputText', requestPayload.DNTCaptchaInputText)
formData.append('DNTCaptchaText', requestPayload.DNTCaptchaText) if (payload.captcha) {
formData.append('DNTCaptchaToken', requestPayload.DNTCaptchaToken) formData.append('DNTCaptchaInputText', payload.captcha.DNTCaptchaInputText)
formData.append('DNTCaptchaText', payload.captcha.DNTCaptchaText)
formData.append('DNTCaptchaToken', payload.captcha.DNTCaptchaToken)
}
const { data } = await authApi.login(formData, { const { data } = await authApi.login(formData, {
signal: loginController.value.signal, signal: loginController.value.signal,
@@ -130,10 +101,6 @@ export const useAuthStore = defineStore('auth', () => {
} }
return { return {
getCaptcha,
captcha,
captchaLoading,
captchaErrorMessage,
user, user,
token, token,
loading, loading,
+7 -1
View File
@@ -16,8 +16,14 @@ export interface CaptchaResponse {
dntCaptchaTextValue: string dntCaptchaTextValue: string
} }
export interface LoginCaptchaPayload {
DNTCaptchaInputText: string
DNTCaptchaText: string
DNTCaptchaToken: string
}
export interface LoginPayload { export interface LoginPayload {
UserID: string UserID: string
Password: string Password: string
DNTCaptchaInputText: string captcha?: LoginCaptchaPayload
} }
+32 -33
View File
@@ -8,6 +8,7 @@
:layout="formPositionLayout" :layout="formPositionLayout"
:mobile-announcement="mobileAnnouncement" :mobile-announcement="mobileAnnouncement"
:toolbar="toolbar" :toolbar="toolbar"
:with-announcement="withAnnouncement"
@captcha-change="handleCaptchaChange" @captcha-change="handleCaptchaChange"
@captcha-refresh="handleCaptchaRefresh" @captcha-refresh="handleCaptchaRefresh"
@change-locale="handleChangeLocale" @change-locale="handleChangeLocale"
@@ -52,17 +53,17 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { storeToRefs } from 'pinia'
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import HyakkaouAcademyImage from '@/assets/logo.png' import HyakkaouAcademyImage from '@/assets/logo.png'
import PageLogin from '@/components/PageLogin.vue' import PageLogin from '@/components/PageLogin.vue'
import { useAuthStore } from '@/stores/auth'
import { import {
type LoginAnnouncementListItem, type LoginAnnouncementListItem,
useLoginAnnouncementsStore, useLoginAnnouncements,
} from '@/stores/loginAnnouncements' } from '@/composables/useLoginAnnouncements'
import { useLoginCaptcha } from '@/composables/useLoginCaptcha'
import { useAuthStore } from '@/stores/auth'
import { useMenuStore } from '@/stores/menu' import { useMenuStore } from '@/stores/menu'
import { useSnackbarStore } from '@/stores/snackbar' import { useSnackbarStore } from '@/stores/snackbar'
@@ -73,15 +74,8 @@ const { t, locale } = useI18n()
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const authStore = useAuthStore() const authStore = useAuthStore()
const loginAnnouncementsStore = useLoginAnnouncementsStore()
const menuStore = useMenuStore() const menuStore = useMenuStore()
const snackbarStore = useSnackbarStore() const snackbarStore = useSnackbarStore()
const {
boardConfig: announcementBoard,
mobileAnnouncementConfig: mobileAnnouncement,
selectedAnnouncement,
selectedAnnouncementDetail,
} = storeToRefs(loginAnnouncementsStore)
// 語系選項 // 語系選項
const locales = ['zh-TW', 'en-US'] const locales = ['zh-TW', 'en-US']
@@ -91,9 +85,21 @@ const illustrationImage = ref(HyakkaouAcademyImage)
// 功能開關與版型 // 功能開關與版型
const formPositionLayout = ref<LayoutType>('side-left') const formPositionLayout = ref<LayoutType>('side-left')
// 是否啟用公告
const withAnnouncement = ref(true)
const withForgotPassword = ref(true)
const withRememberAccount = ref(true)
// 功能開關:是否啟用驗證碼 // 功能開關:是否啟用驗證碼
const withCaptcha = ref(true) const withCaptcha = ref(true)
const loginCaptcha = useLoginCaptcha({ enabled: withCaptcha })
const loginAnnouncements = useLoginAnnouncements({ enabled: withAnnouncement })
const {
boardConfig: announcementBoard,
mobileAnnouncementConfig: mobileAnnouncement,
selectedAnnouncement,
selectedAnnouncementDetail,
} = loginAnnouncements
// 文字內容(i18n // 文字內容(i18n
const title = computed(() => t('pages.login.title')) const title = computed(() => t('pages.login.title'))
@@ -117,9 +123,6 @@ const forgotPasswordHref = ref('/forgot-password')
const forgotPasswordTarget = ref<string | undefined>(undefined) const forgotPasswordTarget = ref<string | undefined>(undefined)
// 記住帳號的 localStorage key // 記住帳號的 localStorage key
const rememberStorageKey = ref('login.remember.username') const rememberStorageKey = ref('login.remember.username')
// 驗證碼 API
const captchaValue = ref('')
// 驗證與對話框狀態 // 驗證與對話框狀態
const dialogVisible = ref(false) const dialogVisible = ref(false)
const dialogTitle = ref('') const dialogTitle = ref('')
@@ -153,18 +156,14 @@ const form = computed(() => ({
captchaPlaceholder: captchaPlaceholder.value, captchaPlaceholder: captchaPlaceholder.value,
refreshTitle: refreshTitle.value, refreshTitle: refreshTitle.value,
rememberStorageKey: rememberStorageKey.value, rememberStorageKey: rememberStorageKey.value,
withForgotPassword: withForgotPassword.value,
withRememberAccount: withRememberAccount.value,
// 功能開關:是否顯示驗證碼 // 功能開關:是否顯示驗證碼
withCaptcha: withCaptcha.value, withCaptcha: withCaptcha.value,
captcha: authStore.captcha captcha: loginCaptcha.formCaptcha.value,
? { captchaValue: loginCaptcha.captchaValue.value,
imgUrl: authStore.captcha.dntCaptchaImgUrl, captchaLoading: loginCaptcha.captchaLoading.value,
id: authStore.captcha.dntCaptchaId, captchaErrorMessage: loginCaptcha.captchaErrorMessage.value ?? '',
tokenValue: authStore.captcha.dntCaptchaTokenValue,
}
: undefined,
captchaValue: captchaValue.value,
captchaLoading: authStore.captchaLoading,
captchaErrorMessage: authStore.captchaErrorMessage ?? '',
captchaVerified: false, captchaVerified: false,
forgotPassword: { forgotPassword: {
text: forgotPasswordText.value, text: forgotPasswordText.value,
@@ -183,6 +182,8 @@ const toolbar = computed(() => ({
// 事件處理 // 事件處理
function handleForgotPassword(e: MouseEvent) { function handleForgotPassword(e: MouseEvent) {
if (!withForgotPassword.value) return
console.log('Forgot Password Click:', e) console.log('Forgot Password Click:', e)
} }
@@ -192,12 +193,11 @@ function handleChangeLocale(nextLocale: string) {
} }
async function handleCaptchaRefresh() { async function handleCaptchaRefresh() {
captchaValue.value = '' await loginCaptcha.refreshCaptcha().catch(() => undefined)
await authStore.getCaptcha()
} }
function handleCaptchaChange(value: string) { function handleCaptchaChange(value: string) {
captchaValue.value = value loginCaptcha.setCaptchaValue(value)
} }
function handleToggleLayout() { function handleToggleLayout() {
@@ -208,12 +208,12 @@ function handleToggleLayout() {
} }
function handleSelectAnnouncement(item: LoginAnnouncementListItem) { function handleSelectAnnouncement(item: LoginAnnouncementListItem) {
loginAnnouncementsStore.selectById(item.id) loginAnnouncements.selectById(item.id)
announcementDialogVisible.value = true announcementDialogVisible.value = true
} }
async function onLogin(data: Record<string, unknown>) { async function onLogin(data: Record<string, unknown>) {
if (withCaptcha.value && !captchaValue.value) { if (withCaptcha.value && !loginCaptcha.captchaValue.value) {
dialogTitle.value = t('common.notice') dialogTitle.value = t('common.notice')
dialogMessage.value = t('pages.login.alert.verifyRequired') dialogMessage.value = t('pages.login.alert.verifyRequired')
dialogVisible.value = true dialogVisible.value = true
@@ -234,7 +234,7 @@ async function onLogin(data: Record<string, unknown>) {
await authStore.login({ await authStore.login({
UserID: userId, UserID: userId,
Password: password, Password: password,
DNTCaptchaInputText: captchaValue.value, captcha: loginCaptcha.getLoginCaptchaPayload(),
}) })
menuStore.getMenu(authStore.user?.id ?? '') menuStore.getMenu(authStore.user?.id ?? '')
@@ -260,8 +260,7 @@ async function onLogin(data: Record<string, unknown>) {
} }
onMounted(() => { onMounted(() => {
loginAnnouncementsStore.hydrate() void loginAnnouncements.load()
loginAnnouncementsStore.fetchMobileAnnouncements() void loginCaptcha.loadCaptcha().catch(() => undefined)
authStore.getCaptcha()
}) })
</script> </script>
+7 -4
View File
@@ -1,10 +1,13 @@
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from 'node:url'
import Vue from '@vitejs/plugin-vue' import Vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite' import { defineConfig, loadEnv } from 'vite'
import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify' import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
return {
plugins: [ plugins: [
Vue({ Vue({
template: { transformAssetUrls }, template: { transformAssetUrls },
@@ -36,7 +39,7 @@ export default defineConfig({
port: 3700, port: 3700,
proxy:{ proxy:{
"/service/": { "/service/": {
target: process.env.VITE_PROXY_TARGET || "http://localhost:8080", target: env.VITE_PROXY_TARGET || "http://localhost:8080",
changeOrigin: true, changeOrigin: true,
}, },
} }
@@ -47,4 +50,4 @@ export default defineConfig({
}, },
}, },
}, },
}) }})