feat(stores): add Pinia domain stores and update docs

Implement concrete Pinia stores for app UI and domain data instead of
placeholder re-exports, including seeded student records and snackbar state.

Refresh README guidance for components, plugins, and services to document the
current project structure, data flow, and usage conventions.feat(stores): add Pinia domain stores and update docs

Implement concrete Pinia stores for app UI and domain data instead of
placeholder re-exports, including seeded student records and snackbar state.

Refresh README guidance for components, plugins, and services to document the
current project structure, data flow, and usage conventions.
This commit is contained in:
skytek_xinliang
2026-05-05 11:54:19 +08:00
parent 6eab4d9744
commit b37f4363eb
23 changed files with 1531 additions and 1588 deletions
+209 -1
View File
@@ -1 +1,209 @@
export * from './stores/loginAnnouncements'
import { defineStore } from 'pinia'
import { computed, ref, watch } from 'vue'
export interface LoginAnnouncementItem {
id: string | number
date: string
school: string
title: string
tab?: string
detail: string
}
export interface LoginAnnouncementListItem {
id: string | number
date: string
school: string
title: string
tab?: string
}
export interface LoginMobileAnnouncementItem {
id: string | number
content: string
title?: string
createdAt?: string
}
const storageKey = 'sk_playground_login_announcements'
const defaultItems: LoginAnnouncementItem[] = [
{
id: 'announcement-1',
date: '2024-03-19',
school: '市立實踐國中',
title: '臺北市立實踐國中徵求113學年度教學支援工作人員',
tab: 'junior',
detail: '公告內容:本校辦理本土語教學支援工作人員甄選,請於期限內完成報名與資料繳交。',
},
{
id: 'announcement-2',
date: '2023-12-12',
school: '市立華江高中',
title: '臺北市立華江高級中學112學年度第二學期本土語教學支援人員甄選',
tab: 'senior',
detail: '公告內容:甄選包含書面審查與面試,相關時間地點請參閱簡章附件。',
},
{
id: 'announcement-3',
date: '2023-12-05',
school: '市立麗山高中',
title: '內湖區麗山高中誠徵閩南語教支人員數名',
tab: 'senior',
detail: '公告內容:需具備相關教學經驗,錄取後依課務需求排課。',
},
{
id: 'announcement-4',
date: '2023-11-28',
school: '市立永吉國中',
title: '公告本市學校本土語教學支援人員報名資訊',
tab: 'junior',
detail: '公告內容:統一受理報名,請依公告流程檢附文件並完成線上登錄。',
},
{
id: 'announcement-5',
date: '2023-11-21',
school: '市立百齡高中',
title: '112學年度本土語文教學支援工作人員甄選簡章',
tab: 'senior',
detail: '公告內容:簡章含資格條件、甄選方式、成績計算與錄取標準。',
},
{
id: 'announcement-6',
date: '2023-11-10',
school: '市立成德國中',
title: '本土語教學支援工作人員甄選(第二次)',
tab: 'junior',
detail: '公告內容:第二次甄選開放補件,報名截止日以公告為準。',
},
]
function readItems(): LoginAnnouncementItem[] {
if (typeof window === 'undefined') return defaultItems
try {
const raw = window.localStorage.getItem(storageKey)
if (!raw) return defaultItems
const parsed = JSON.parse(raw)
return Array.isArray(parsed) ? (parsed as LoginAnnouncementItem[]) : defaultItems
} catch {
return defaultItems
}
}
function writeItems(items: LoginAnnouncementItem[]) {
if (typeof window === 'undefined') return
try {
window.localStorage.setItem(storageKey, JSON.stringify(items))
} catch {
return
}
}
async function mockFetchMobileAnnouncementsApi(): Promise<LoginMobileAnnouncementItem[]> {
return [
{
id: 'mobile-announcement-1',
content: '系統正常運行中',
title: '系統公告',
createdAt: '2026-02-11',
},
]
}
export const useLoginAnnouncementsStore = defineStore('loginAnnouncements', () => {
const items = ref<LoginAnnouncementItem[]>(readItems())
const selectedId = ref<string | number | null>(null)
const mobileAnnouncements = ref<LoginMobileAnnouncementItem[]>([])
const listItems = computed<LoginAnnouncementListItem[]>(() =>
items.value.map((item) => ({
id: item.id,
date: item.date,
school: item.school,
title: item.title,
tab: item.tab,
}))
)
const boardConfig = computed(() => ({
title: '學校公告區',
tabs: [
{ label: '全部', value: '__all__' },
{ label: '國中', value: 'junior' },
{ label: '高中', value: 'senior' },
],
items: listItems.value,
systemAnnouncements: mobileAnnouncements.value,
itemsPerPage: 5,
dateHeader: '公告時間',
schoolHeader: '公告學校',
titleHeader: '公告標題',
paginationLabel: '總筆數:',
}))
const selectedAnnouncement = computed(() => {
if (selectedId.value === null) return null
return items.value.find((item) => item.id === selectedId.value) ?? null
})
const selectedAnnouncementDetail = computed(() => {
return selectedAnnouncement.value?.detail ?? ''
})
const mobileAnnouncementConfig = computed(() => ({
items: mobileAnnouncements.value,
show: mobileAnnouncements.value.length > 0,
viewAllText: '查看全部',
listTitle: '系統公告',
closeText: '關閉',
emptyText: '目前沒有公告',
}))
const hydrate = () => {
items.value = readItems()
}
const replaceAll = (nextItems: LoginAnnouncementItem[]) => {
items.value = Array.isArray(nextItems) ? nextItems : []
}
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 () => {
await fetchMobileAnnouncements()
}
watch(
items,
(val) => {
writeItems(val)
},
{ deep: true }
)
return {
items,
listItems,
boardConfig,
mobileAnnouncementConfig,
selectedAnnouncement,
selectedAnnouncementDetail,
hydrate,
replaceAll,
selectById,
clearSelection,
fetchMobileAnnouncements,
fetchMobileAnnouncement,
}
})