feat: 公告開關

This commit is contained in:
skytek_xinliang
2026-05-22 10:30:04 +08:00
parent 8cf5aacf21
commit 8378c44ad7
3 changed files with 55 additions and 47 deletions
+9 -1
View File
@@ -18,6 +18,7 @@
<LoginBrand :title="props.branding.title" />
</div>
<v-sheet
v-if="props.withAnnouncement"
class="board-wrapper pa-2 pa-lg-0"
color="rgba(var(--v-theme-surface), 0.8)"
elevation="0"
@@ -195,7 +196,11 @@
</v-card>
</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-title class="text-subtitle-1 font-weight-bold">
{{ props.mobileAnnouncement.listTitle }}
@@ -330,6 +335,7 @@ interface ToolBarConfig {
interface Props {
layout: 'side-left' | 'side-right' | 'card'
withAnnouncement?: boolean
branding: BrandingConfig
illustration: IllustrationConfig
announcementBoard: AnnouncementBoardConfig
@@ -341,6 +347,7 @@ interface Props {
const props = withDefaults(defineProps<Props>(), {
layout: 'side-left',
withAnnouncement: true,
branding: () => ({
title: 'Skyteck Login',
organization: 'school',
@@ -430,6 +437,7 @@ const mobileAnnouncementSheetVisible = ref(false)
const mobileAnnouncementItems = computed(() => props.mobileAnnouncement.items ?? [])
const showMobileAnnouncementBanner = computed(() => {
if (!props.withAnnouncement) return false
if (props.mobileAnnouncement.show === false) return false
return mobileAnnouncementItems.value.length > 0
})
@@ -1,5 +1,4 @@
import { defineStore } from 'pinia'
import { computed, ref, watch } from 'vue'
import { computed, ref, toValue, watch, type MaybeRefOrGetter } from 'vue'
export interface LoginAnnouncementItem {
id: string | number
@@ -25,6 +24,10 @@ export interface LoginMobileAnnouncementItem {
createdAt?: string
}
interface UseLoginAnnouncementsOptions {
enabled: MaybeRefOrGetter<boolean>
}
const storageKey = 'sk_playground_login_announcements'
const defaultItems: LoginAnnouncementItem[] = [
@@ -110,10 +113,11 @@ async function mockFetchMobileAnnouncementsApi(): Promise<LoginMobileAnnouncemen
]
}
export const useLoginAnnouncementsStore = defineStore('loginAnnouncements', () => {
const items = ref<LoginAnnouncementItem[]>(readItems())
export function useLoginAnnouncements(options: UseLoginAnnouncementsOptions) {
const items = ref<LoginAnnouncementItem[]>([])
const selectedId = ref<string | number | null>(null)
const mobileAnnouncements = ref<LoginMobileAnnouncementItem[]>([])
const enabled = computed(() => toValue(options.enabled))
const listItems = computed<LoginAnnouncementListItem[]>(() =>
items.value.map((item) => ({
@@ -132,8 +136,8 @@ export const useLoginAnnouncementsStore = defineStore('loginAnnouncements', () =
{ label: '國中', value: 'junior' },
{ label: '高中', value: 'senior' },
],
items: listItems.value,
systemAnnouncements: mobileAnnouncements.value,
items: enabled.value ? listItems.value : [],
systemAnnouncements: enabled.value ? mobileAnnouncements.value : [],
itemsPerPage: 5,
dateHeader: '公告時間',
schoolHeader: '公告學校',
@@ -142,7 +146,7 @@ export const useLoginAnnouncementsStore = defineStore('loginAnnouncements', () =
}))
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
})
@@ -151,59 +155,54 @@ export const useLoginAnnouncementsStore = defineStore('loginAnnouncements', () =
})
const mobileAnnouncementConfig = computed(() => ({
items: mobileAnnouncements.value,
show: mobileAnnouncements.value.length > 0,
items: enabled.value ? mobileAnnouncements.value : [],
show: enabled.value && mobileAnnouncements.value.length > 0,
viewAllText: '查看全部',
listTitle: '系統公告',
closeText: '關閉',
emptyText: '目前沒有公告',
}))
const hydrate = () => {
function hydrate() {
if (!enabled.value) return
items.value = readItems()
}
const replaceAll = (nextItems: LoginAnnouncementItem[]) => {
items.value = Array.isArray(nextItems) ? nextItems : []
}
async function fetchMobileAnnouncements() {
if (!enabled.value) return
const selectById = (id: string | number) => {
selectedId.value = id
}
const clearSelection = () => {
selectedId.value = null
}
const fetchMobileAnnouncements = async () => {
const result = await mockFetchMobileAnnouncementsApi()
mobileAnnouncements.value = Array.isArray(result) ? result : []
}
const fetchMobileAnnouncement = async () => {
async function load() {
hydrate()
await fetchMobileAnnouncements()
}
function selectById(id: string | number) {
if (!enabled.value) return
selectedId.value = id
}
watch(
items,
(val) => {
if (!enabled.value) return
writeItems(val)
},
{ deep: true }
)
return {
items,
listItems,
boardConfig,
mobileAnnouncementConfig,
selectedAnnouncement,
selectedAnnouncementDetail,
hydrate,
replaceAll,
load,
selectById,
clearSelection,
fetchMobileAnnouncements,
fetchMobileAnnouncement,
}
})
}
+16 -15
View File
@@ -8,6 +8,7 @@
:layout="formPositionLayout"
:mobile-announcement="mobileAnnouncement"
:toolbar="toolbar"
:with-announcement="withAnnouncement"
@captcha-change="handleCaptchaChange"
@captcha-refresh="handleCaptchaRefresh"
@change-locale="handleChangeLocale"
@@ -52,18 +53,17 @@
</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 PageLogin from '@/components/PageLogin.vue'
import { useLoginCaptcha } from '@/composables/useLoginCaptcha'
import { useAuthStore } from '@/stores/auth'
import {
type LoginAnnouncementListItem,
useLoginAnnouncementsStore,
} from '@/stores/loginAnnouncements'
useLoginAnnouncements,
} from '@/composables/useLoginAnnouncements'
import { useLoginCaptcha } from '@/composables/useLoginCaptcha'
import { useAuthStore } from '@/stores/auth'
import { useMenuStore } from '@/stores/menu'
import { useSnackbarStore } from '@/stores/snackbar'
@@ -74,15 +74,8 @@ 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']
@@ -92,10 +85,19 @@ const illustrationImage = ref(HyakkaouAcademyImage)
// 功能開關與版型
const formPositionLayout = ref<LayoutType>('side-left')
// 是否啟用公告
const withAnnouncement = 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
const title = computed(() => t('pages.login.title'))
@@ -200,7 +202,7 @@ function handleToggleLayout() {
}
function handleSelectAnnouncement(item: LoginAnnouncementListItem) {
loginAnnouncementsStore.selectById(item.id)
loginAnnouncements.selectById(item.id)
announcementDialogVisible.value = true
}
@@ -252,8 +254,7 @@ async function onLogin(data: Record<string, unknown>) {
}
onMounted(() => {
loginAnnouncementsStore.hydrate()
loginAnnouncementsStore.fetchMobileAnnouncements()
void loginAnnouncements.load()
void loginCaptcha.loadCaptcha().catch(() => undefined)
})
</script>