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" /> <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"
@@ -195,7 +196,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 }}
@@ -330,6 +335,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 +347,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',
@@ -430,6 +437,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
}) })
@@ -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,
} }
}) }
+16 -15
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,18 +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 { useLoginCaptcha } from '@/composables/useLoginCaptcha'
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'
@@ -74,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']
@@ -92,10 +85,19 @@ const illustrationImage = ref(HyakkaouAcademyImage)
// 功能開關與版型 // 功能開關與版型
const formPositionLayout = ref<LayoutType>('side-left') const formPositionLayout = ref<LayoutType>('side-left')
// 是否啟用公告
const withAnnouncement = ref(true)
// 功能開關:是否啟用驗證碼 // 功能開關:是否啟用驗證碼
const withCaptcha = ref(true) const withCaptcha = ref(true)
const loginCaptcha = useLoginCaptcha({ enabled: withCaptcha }) 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'))
@@ -200,7 +202,7 @@ function handleToggleLayout() {
} }
function handleSelectAnnouncement(item: LoginAnnouncementListItem) { function handleSelectAnnouncement(item: LoginAnnouncementListItem) {
loginAnnouncementsStore.selectById(item.id) loginAnnouncements.selectById(item.id)
announcementDialogVisible.value = true announcementDialogVisible.value = true
} }
@@ -252,8 +254,7 @@ async function onLogin(data: Record<string, unknown>) {
} }
onMounted(() => { onMounted(() => {
loginAnnouncementsStore.hydrate() void loginAnnouncements.load()
loginAnnouncementsStore.fetchMobileAnnouncements()
void loginCaptcha.loadCaptcha().catch(() => undefined) void loginCaptcha.loadCaptcha().catch(() => undefined)
}) })
</script> </script>