2025-01-18 JavaScript 系列八:第4課 ── 行事曆應用程式-步驟08

2025-01-18 JavaScript 系列八:第4課 ── 行事曆應用程式-步驟08

把資料存在 local storage
好!超棒!💪
你已經完成在 FullCalendar 上新增活動了,
下一步:要做到 ➔ 「把活動存到 localStorage」✅

這個功能目標是:

  • 每次新增活動時,存一份到 localStorage
  • 下次打開網頁時,localStorage 的活動可以自動載入到 FullCalendar

✨【完整加上 localStorage 版】✨

我直接在你剛剛那一版上,加上 localStorage 功能喔:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
<template>
<div>
<h2>我的行事曆</h2>
<div ref="calendarRef" id="calendar"></div>
</div>

<!-- 新增活動 Modal -->
<div v-if="showForm" class="modal-overlay">
<div class="modal">
<h3>新增活動</h3>
<label>
活動名稱:
<input v-model="formData.title" type="text">
</label>
<label>
活動日期:
<input v-model="formData.date" type="date">
</label>
<label>
活動時間:
<input v-model="formData.time" type="time">
</label>
<div class="modal-buttons">
<button @click="saveEvent">儲存</button>
<button @click="showForm = false">取消</button>
</div>
</div>
</div>
</template>

<script setup>
import { ref, onMounted } from 'vue'

const calendarRef = ref(null)
const showForm = ref(false)
const formData = ref({
title: '',
date: '',
time: ''
})

let calendar = null

const STORAGE_KEY = 'calendar-events' // 👈 localStorage 專用 key 名

onMounted(() => {
if (window.FullCalendar && window.FullCalendar.Calendar) {
calendar = new window.FullCalendar.Calendar(calendarRef.value, {
initialView: 'dayGridMonth',
locale: 'zh-tw',
events: loadEventsFromLocalStorage(), // 👈 初始化時載入 localStorage
dateClick(info) {
document.querySelectorAll('.add-event-button').forEach(btn => btn.remove())
document.querySelectorAll('.selected-day').forEach(el => el.classList.remove('selected-day'))

const cellEl = info.dayEl.querySelector('.fc-daygrid-day-frame')
if (!cellEl) return

cellEl.classList.add('selected-day')

const button = document.createElement('button')
button.className = 'add-event-button'
button.innerHTML = `<span class="text">新增活動</span><span class="icon">➕</span>`

button.onclick = () => {
formData.value.date = info.dateStr
formData.value.time = '09:00'
formData.value.title = ''
showForm.value = true
}

cellEl.appendChild(button)
}
})
calendar.render()
} else {
console.error('FullCalendar 尚未載入,請確認 CDN script')
}
})

function saveEvent() {
if (!formData.value.title) {
alert('請輸入活動名稱')
return
}

const event = {
title: formData.value.title,
start: `${formData.value.date}T${formData.value.time}`
}

calendar.addEvent(event)
saveEventToLocalStorage(event) // 👈 儲存到 localStorage

showForm.value = false
}

function saveEventToLocalStorage(event) {
const events = JSON.parse(localStorage.getItem(STORAGE_KEY)) || []
events.push(event)
localStorage.setItem(STORAGE_KEY, JSON.stringify(events))
}

function loadEventsFromLocalStorage() {
return JSON.parse(localStorage.getItem(STORAGE_KEY)) || []
}
</script>

<style scoped>
/* 🔥 之前的樣式不變 */
#calendar {
max-width: 800px;
margin: 20px auto;
}

:deep(.selected-day) {
background-color: #f0f0f0 !important;
transition: background-color 0.3s ease;
}

:deep(.add-event-button) {
background-color: #1e2b37;
color: white;
border: none;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
}

:deep(.add-event-button .icon) {
display: none;
}
:deep(.add-event-button .text) {
display: inline;
}
@media (max-width: 600px) {
:deep(.add-event-button .text) {
display: none;
}
:deep(.add-event-button .icon) {
display: inline;
font-size: 16px;
}
}

.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}

.modal {
background: white;
padding: 20px;
border-radius: 8px;
width: 90%;
max-width: 300px;
}

.modal label {
display: block;
margin-bottom: 10px;
}

.modal input {
width: 100%;
padding: 4px;
box-sizing: border-box;
}

.modal-buttons {
display: flex;
justify-content: space-between;
margin-top: 15px;
}
</style>

🎯【現在功能完整】

  • 🔵 加活動 ➡️ localStorage 自動存起來
  • 🔵 重新整理網頁 ➡️ 活動不會消失
  • 🔵 每次加新活動 ➡️ 自動同步 localStorage

📦 整理一下,今天你學會了:

主題 重點
FullCalendar 基礎使用 初始畫面、點擊事件
Modal 表單互動 v-if 顯示/隱藏、v-model 資料綁定
操作 localStorage JSON.parse()JSON.stringify()
RWD 響應式按鈕 手機版按鈕自動變簡單

要不要【下一步升級】:
「刪除活動 + localStorage 同步更新」
(這樣就可以點活動 ➔ 刪除 ➔ localStorage 自動跟著刪)

要繼續的話,回我【刪除功能】~🚀
要嗎?😄