picocrank 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/router.js +11 -0
- package/vue/components/Calendar.vue +696 -0
- package/vue/components/Pagination.vue +71 -88
- package/vue/views/App.vue +1 -0
- package/vue/views/CalendarExample.vue +328 -0
- package/vue/views/TableExample.vue +56 -1
package/package.json
CHANGED
package/router.js
CHANGED
|
@@ -4,6 +4,7 @@ import { HomeIcon } from '@hugeicons/core-free-icons';
|
|
|
4
4
|
import { TableIcon } from '@hugeicons/core-free-icons';
|
|
5
5
|
import { ViewIcon } from '@hugeicons/core-free-icons';
|
|
6
6
|
import { SecurityValidationIcon } from '@hugeicons/core-free-icons';
|
|
7
|
+
import { CalendarIcon } from '@hugeicons/core-free-icons';
|
|
7
8
|
|
|
8
9
|
const routes = [
|
|
9
10
|
{
|
|
@@ -43,6 +44,16 @@ const routes = [
|
|
|
43
44
|
title: 'Admin',
|
|
44
45
|
icon: SecurityValidationIcon,
|
|
45
46
|
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'CalendarExample',
|
|
50
|
+
path: '/calendar-example',
|
|
51
|
+
component: () => import('./vue/views/CalendarExample.vue'),
|
|
52
|
+
props: true,
|
|
53
|
+
meta: {
|
|
54
|
+
title: 'Calendar Example',
|
|
55
|
+
icon: CalendarIcon,
|
|
56
|
+
}
|
|
46
57
|
}
|
|
47
58
|
];
|
|
48
59
|
|
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
export interface CalendarEvent {
|
|
5
|
+
id: string | number
|
|
6
|
+
title: string
|
|
7
|
+
startDate?: Date | string | null
|
|
8
|
+
endDate?: Date | string | null
|
|
9
|
+
date?: Date | string | null
|
|
10
|
+
[key: string]: any
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface CalendarProps {
|
|
14
|
+
events: CalendarEvent[]
|
|
15
|
+
monthNames?: string[]
|
|
16
|
+
dayNames?: string[]
|
|
17
|
+
loading?: boolean
|
|
18
|
+
error?: string | null
|
|
19
|
+
// Formatter functions
|
|
20
|
+
getEventDate?: (event: CalendarEvent) => Date | null
|
|
21
|
+
getEventDateRange?: (event: CalendarEvent) => { start: Date | null; end: Date | null }
|
|
22
|
+
formatEventTime?: (event: CalendarEvent, date: Date) => string
|
|
23
|
+
// Customization
|
|
24
|
+
showNavigation?: boolean
|
|
25
|
+
currentMonth?: number
|
|
26
|
+
currentYear?: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const props = withDefaults(defineProps<CalendarProps>(), {
|
|
30
|
+
section: false,
|
|
31
|
+
monthNames: () => [
|
|
32
|
+
'January', 'February', 'March', 'April', 'May', 'June',
|
|
33
|
+
'July', 'August', 'September', 'October', 'November', 'December'
|
|
34
|
+
],
|
|
35
|
+
dayNames: () => ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
|
|
36
|
+
loading: false,
|
|
37
|
+
error: null,
|
|
38
|
+
showNavigation: true,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const emit = defineEmits<{
|
|
42
|
+
'event-click': [event: CalendarEvent]
|
|
43
|
+
'date-click': [date: Date]
|
|
44
|
+
'month-change': [month: number, year: number]
|
|
45
|
+
'event-context-menu': [event: CalendarEvent, mouseEvent: MouseEvent]
|
|
46
|
+
}>()
|
|
47
|
+
|
|
48
|
+
// Internal calendar state
|
|
49
|
+
const internalCurrentDate = ref(new Date())
|
|
50
|
+
const currentDate = computed(() => {
|
|
51
|
+
if (props.currentMonth !== undefined && props.currentYear !== undefined) {
|
|
52
|
+
return new Date(props.currentYear, props.currentMonth, 1)
|
|
53
|
+
}
|
|
54
|
+
return internalCurrentDate.value
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const currentMonth = computed(() => currentDate.value.getMonth())
|
|
58
|
+
const currentYear = computed(() => currentDate.value.getFullYear())
|
|
59
|
+
|
|
60
|
+
// Helper function to check if a time is midnight (00:00)
|
|
61
|
+
function isMidnight(dateValue: any): boolean {
|
|
62
|
+
if (!dateValue) return false
|
|
63
|
+
const date = new Date(dateValue)
|
|
64
|
+
return date.getHours() === 0 && date.getMinutes() === 0
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Helper function to format time, returning "No time" if midnight
|
|
68
|
+
function formatTimeOrNoTime(dateValue: any): string {
|
|
69
|
+
if (!dateValue) return 'No time'
|
|
70
|
+
if (isMidnight(dateValue)) return 'No time'
|
|
71
|
+
const date = new Date(dateValue)
|
|
72
|
+
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Get event date range
|
|
76
|
+
function getEventDateRange(event: CalendarEvent): { start: Date | null; end: Date | null } {
|
|
77
|
+
if (props.getEventDateRange) {
|
|
78
|
+
return props.getEventDateRange(event)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Default implementation
|
|
82
|
+
let start: Date | null = null
|
|
83
|
+
let end: Date | null = null
|
|
84
|
+
|
|
85
|
+
if (event.startDate) {
|
|
86
|
+
start = new Date(event.startDate)
|
|
87
|
+
} else if (event.date) {
|
|
88
|
+
start = new Date(event.date)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (event.endDate) {
|
|
92
|
+
end = new Date(event.endDate)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { start, end }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Get events for a specific date
|
|
99
|
+
function getEventsForDate(date: Date): CalendarEvent[] {
|
|
100
|
+
const targetDate = new Date(date)
|
|
101
|
+
targetDate.setHours(0, 0, 0, 0)
|
|
102
|
+
|
|
103
|
+
return props.events.filter(event => {
|
|
104
|
+
const { start, end } = getEventDateRange(event)
|
|
105
|
+
|
|
106
|
+
if (!start) return false
|
|
107
|
+
|
|
108
|
+
const startDateOnly = new Date(start)
|
|
109
|
+
startDateOnly.setHours(0, 0, 0, 0)
|
|
110
|
+
|
|
111
|
+
if (end) {
|
|
112
|
+
const endDateOnly = new Date(end)
|
|
113
|
+
endDateOnly.setHours(0, 0, 0, 0)
|
|
114
|
+
return targetDate >= startDateOnly && targetDate <= endDateOnly
|
|
115
|
+
} else {
|
|
116
|
+
return targetDate.getTime() === startDateOnly.getTime()
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check if an event is a multi-day event
|
|
122
|
+
function isMultiDayEvent(event: CalendarEvent): boolean {
|
|
123
|
+
const { start, end } = getEventDateRange(event)
|
|
124
|
+
|
|
125
|
+
if (!start || !end) return false
|
|
126
|
+
|
|
127
|
+
const startDate = new Date(start)
|
|
128
|
+
const endDate = new Date(end)
|
|
129
|
+
|
|
130
|
+
startDate.setHours(0, 0, 0, 0)
|
|
131
|
+
endDate.setHours(0, 0, 0, 0)
|
|
132
|
+
|
|
133
|
+
return startDate.getTime() !== endDate.getTime()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Get the position of an event within a multi-day range for a specific date
|
|
137
|
+
function getMultiDayPosition(event: CalendarEvent, date: Date): 'start' | 'middle' | 'end' | 'single' {
|
|
138
|
+
const { start, end } = getEventDateRange(event)
|
|
139
|
+
|
|
140
|
+
if (!start || !end) return 'single'
|
|
141
|
+
|
|
142
|
+
const startDate = new Date(start)
|
|
143
|
+
const endDate = new Date(end)
|
|
144
|
+
const targetDate = new Date(date)
|
|
145
|
+
|
|
146
|
+
startDate.setHours(0, 0, 0, 0)
|
|
147
|
+
endDate.setHours(0, 0, 0, 0)
|
|
148
|
+
targetDate.setHours(0, 0, 0, 0)
|
|
149
|
+
|
|
150
|
+
if (startDate.getTime() === endDate.getTime()) return 'single'
|
|
151
|
+
if (targetDate.getTime() === startDate.getTime()) return 'start'
|
|
152
|
+
if (targetDate.getTime() === endDate.getTime()) return 'end'
|
|
153
|
+
if (targetDate > startDate && targetDate < endDate) return 'middle'
|
|
154
|
+
|
|
155
|
+
return 'single'
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Format event time based on position in multi-day event
|
|
159
|
+
function formatEventTimeDefault(event: CalendarEvent, date: Date): string {
|
|
160
|
+
if (props.formatEventTime) {
|
|
161
|
+
return props.formatEventTime(event, date)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const { start, end } = getEventDateRange(event)
|
|
165
|
+
const position = getMultiDayPosition(event, date)
|
|
166
|
+
|
|
167
|
+
if (!start || !end) return 'No time'
|
|
168
|
+
|
|
169
|
+
if (position === 'start') {
|
|
170
|
+
return formatTimeOrNoTime(start)
|
|
171
|
+
} else if (position === 'end') {
|
|
172
|
+
return formatTimeOrNoTime(end)
|
|
173
|
+
} else if (position === 'middle') {
|
|
174
|
+
return 'All day'
|
|
175
|
+
} else if (position === 'single') {
|
|
176
|
+
return formatTimeOrNoTime(start)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return 'All day'
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Calendar generation
|
|
183
|
+
const calendarDays = computed(() => {
|
|
184
|
+
const year = currentYear.value
|
|
185
|
+
const month = currentMonth.value
|
|
186
|
+
|
|
187
|
+
const firstDay = new Date(year, month, 1)
|
|
188
|
+
const lastDay = new Date(year, month + 1, 0)
|
|
189
|
+
const daysInMonth = lastDay.getDate()
|
|
190
|
+
|
|
191
|
+
// Get starting day of week (Monday as first day)
|
|
192
|
+
const startDay = (firstDay.getDay() + 6) % 7
|
|
193
|
+
|
|
194
|
+
const days = []
|
|
195
|
+
|
|
196
|
+
// Add days from previous month to fill the first row
|
|
197
|
+
const prevMonth = month === 0 ? 11 : month - 1
|
|
198
|
+
const prevYear = month === 0 ? year - 1 : year
|
|
199
|
+
const prevMonthLastDay = new Date(prevYear, prevMonth + 1, 0).getDate()
|
|
200
|
+
|
|
201
|
+
for (let i = 0; i < startDay; i++) {
|
|
202
|
+
const day = prevMonthLastDay - startDay + i + 1
|
|
203
|
+
const date = new Date(prevYear, prevMonth, day)
|
|
204
|
+
days.push({
|
|
205
|
+
date,
|
|
206
|
+
events: getEventsForDate(date)
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Add days of the month
|
|
211
|
+
for (let day = 1; day <= daysInMonth; day++) {
|
|
212
|
+
const date = new Date(year, month, day)
|
|
213
|
+
days.push({
|
|
214
|
+
date,
|
|
215
|
+
events: getEventsForDate(date)
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Add days from next month to complete the last row
|
|
220
|
+
const totalCells = 42
|
|
221
|
+
const remainingCells = totalCells - days.length
|
|
222
|
+
|
|
223
|
+
for (let day = 1; day <= remainingCells; day++) {
|
|
224
|
+
const date = new Date(year, month + 1, day)
|
|
225
|
+
days.push({
|
|
226
|
+
date,
|
|
227
|
+
events: getEventsForDate(date)
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return days
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
// Navigation functions
|
|
235
|
+
function previousMonth() {
|
|
236
|
+
if (props.currentMonth !== undefined && props.currentYear !== undefined) {
|
|
237
|
+
const newMonth = currentMonth.value === 0 ? 11 : currentMonth.value - 1
|
|
238
|
+
const newYear = currentMonth.value === 0 ? currentYear.value - 1 : currentYear.value
|
|
239
|
+
emit('month-change', newMonth, newYear)
|
|
240
|
+
} else {
|
|
241
|
+
internalCurrentDate.value = new Date(currentYear.value, currentMonth.value - 1, 1)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function nextMonth() {
|
|
246
|
+
if (props.currentMonth !== undefined && props.currentYear !== undefined) {
|
|
247
|
+
const newMonth = currentMonth.value === 11 ? 0 : currentMonth.value + 1
|
|
248
|
+
const newYear = currentMonth.value === 11 ? currentYear.value + 1 : currentYear.value
|
|
249
|
+
emit('month-change', newMonth, newYear)
|
|
250
|
+
} else {
|
|
251
|
+
internalCurrentDate.value = new Date(currentYear.value, currentMonth.value + 1, 1)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function goToToday() {
|
|
256
|
+
if (props.currentMonth !== undefined && props.currentYear !== undefined) {
|
|
257
|
+
const today = new Date()
|
|
258
|
+
emit('month-change', today.getMonth(), today.getFullYear())
|
|
259
|
+
} else {
|
|
260
|
+
internalCurrentDate.value = new Date()
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Event handlers
|
|
265
|
+
function handleEventClick(event: CalendarEvent) {
|
|
266
|
+
emit('event-click', event)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function handleDateClick(date: Date) {
|
|
270
|
+
emit('date-click', date)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function handleContextMenu(event: CalendarEvent, mouseEvent: MouseEvent) {
|
|
274
|
+
mouseEvent.preventDefault()
|
|
275
|
+
mouseEvent.stopPropagation()
|
|
276
|
+
emit('event-context-menu', event, mouseEvent)
|
|
277
|
+
}
|
|
278
|
+
</script>
|
|
279
|
+
|
|
280
|
+
<template>
|
|
281
|
+
<div class="calendar-wrapper">
|
|
282
|
+
<div v-if="showNavigation" class="calendar-header-nav">
|
|
283
|
+
<h2 class="calendar-title">{{ monthNames[currentMonth] }} {{ currentYear }}</h2>
|
|
284
|
+
<div class="calendar-nav-buttons">
|
|
285
|
+
<slot name="nav-buttons">
|
|
286
|
+
<button @click="previousMonth" class="button neutral">‹</button>
|
|
287
|
+
<button @click="goToToday" class="button neutral">Today</button>
|
|
288
|
+
<button @click="nextMonth" class="button neutral">›</button>
|
|
289
|
+
</slot>
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
<div v-if="error" class="calendar-error">{{ error }}</div>
|
|
294
|
+
<div v-else-if="loading" class="calendar-loading">Loading…</div>
|
|
295
|
+
<div v-else class="calendar-container">
|
|
296
|
+
<div class="calendar-grid">
|
|
297
|
+
<!-- Day headers -->
|
|
298
|
+
<div v-for="day in dayNames" :key="day" class="day-header">{{ day }}</div>
|
|
299
|
+
|
|
300
|
+
<!-- Calendar days -->
|
|
301
|
+
<div
|
|
302
|
+
v-for="(day, index) in calendarDays"
|
|
303
|
+
:key="index"
|
|
304
|
+
class="calendar-day"
|
|
305
|
+
:class="{
|
|
306
|
+
'today': day && day.date.toDateString() === new Date().toDateString(),
|
|
307
|
+
'weekend': day && (day.date.getDay() === 0 || day.date.getDay() === 6),
|
|
308
|
+
'prev-month': day && day.date.getMonth() !== currentMonth && day.date.getMonth() !== (currentMonth + 1) % 12,
|
|
309
|
+
'next-month': day && day.date.getMonth() !== currentMonth && day.date.getMonth() === (currentMonth + 1) % 12
|
|
310
|
+
}"
|
|
311
|
+
>
|
|
312
|
+
<div v-if="day" class="day-content" @click="handleDateClick(day.date)">
|
|
313
|
+
<div class="day-number clickable">
|
|
314
|
+
{{ day.date.getDate() }}
|
|
315
|
+
</div>
|
|
316
|
+
<div class="day-events">
|
|
317
|
+
<div
|
|
318
|
+
v-for="event in day.events.sort((a, b) => {
|
|
319
|
+
const aMultiDay = isMultiDayEvent(a);
|
|
320
|
+
const bMultiDay = isMultiDayEvent(b);
|
|
321
|
+
if (aMultiDay && !bMultiDay) return -1;
|
|
322
|
+
if (!aMultiDay && bMultiDay) return 1;
|
|
323
|
+
return 0;
|
|
324
|
+
}).slice(0, 3)"
|
|
325
|
+
:key="event.id"
|
|
326
|
+
class="calendar-event"
|
|
327
|
+
:class="{
|
|
328
|
+
'multi-day': isMultiDayEvent(event),
|
|
329
|
+
'multi-day-start': isMultiDayEvent(event) && getMultiDayPosition(event, day.date) === 'start',
|
|
330
|
+
'multi-day-middle': isMultiDayEvent(event) && getMultiDayPosition(event, day.date) === 'middle',
|
|
331
|
+
'multi-day-end': isMultiDayEvent(event) && getMultiDayPosition(event, day.date) === 'end'
|
|
332
|
+
}"
|
|
333
|
+
@click.stop="handleEventClick(event)"
|
|
334
|
+
@contextmenu.stop="handleContextMenu(event, $event)"
|
|
335
|
+
>
|
|
336
|
+
<slot name="event" :event="event" :date="day.date" :position="getMultiDayPosition(event, day.date)">
|
|
337
|
+
<div class="event-content">
|
|
338
|
+
<div class="event-title">
|
|
339
|
+
{{ event.title }}
|
|
340
|
+
<span v-if="isMultiDayEvent(event)" class="multi-day-indicator">
|
|
341
|
+
{{ getMultiDayPosition(event, day.date) === 'start' ? '▶' :
|
|
342
|
+
getMultiDayPosition(event, day.date) === 'end' ? '◀' :
|
|
343
|
+
getMultiDayPosition(event, day.date) === 'middle' ? '▬' : '' }}
|
|
344
|
+
</span>
|
|
345
|
+
</div>
|
|
346
|
+
<div class="event-time">
|
|
347
|
+
{{ formatEventTimeDefault(event, day.date) }}
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
</slot>
|
|
351
|
+
</div>
|
|
352
|
+
<div v-if="day.events.length > 3" class="more-events">
|
|
353
|
+
+{{ day.events.length - 3 }} more
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
</template>
|
|
362
|
+
|
|
363
|
+
<style scoped>
|
|
364
|
+
.calendar-wrapper {
|
|
365
|
+
width: 100%;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.calendar-header-nav {
|
|
369
|
+
display: flex;
|
|
370
|
+
justify-content: space-between;
|
|
371
|
+
align-items: center;
|
|
372
|
+
margin-bottom: 1rem;
|
|
373
|
+
padding: 1rem;
|
|
374
|
+
background: #f8f9fa;
|
|
375
|
+
border-radius: 8px;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.calendar-title {
|
|
379
|
+
margin: 0;
|
|
380
|
+
font-size: 1.5rem;
|
|
381
|
+
font-weight: 600;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.calendar-nav-buttons {
|
|
385
|
+
display: flex;
|
|
386
|
+
align-items: center;
|
|
387
|
+
gap: 0.5rem;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.calendar-error {
|
|
391
|
+
color: #b00020;
|
|
392
|
+
padding: 1rem;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.calendar-loading {
|
|
396
|
+
padding: 1rem;
|
|
397
|
+
text-align: center;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.calendar-container {
|
|
401
|
+
background: white;
|
|
402
|
+
border: 1px solid #e0e0e0;
|
|
403
|
+
overflow: hidden;
|
|
404
|
+
border-radius: 8px;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.calendar-grid {
|
|
408
|
+
display: grid;
|
|
409
|
+
grid-template-columns: repeat(7, minmax(0, 1fr));
|
|
410
|
+
min-height: 400px;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.day-header {
|
|
414
|
+
padding: 1rem;
|
|
415
|
+
text-align: center;
|
|
416
|
+
font-weight: 600;
|
|
417
|
+
color: #666;
|
|
418
|
+
border-right: 1px solid #e0e0e0;
|
|
419
|
+
border-bottom: 1px solid #e0e0e0;
|
|
420
|
+
background: #f8f9fa;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.day-header:last-child {
|
|
424
|
+
border-right: none;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.calendar-day {
|
|
428
|
+
border-right: 1px solid #e0e0e0;
|
|
429
|
+
border-bottom: 1px solid #e0e0e0;
|
|
430
|
+
min-height: 120px;
|
|
431
|
+
position: relative;
|
|
432
|
+
transition: background-color 0.2s ease;
|
|
433
|
+
cursor: pointer;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.calendar-day:hover {
|
|
437
|
+
background-color: #f0f8ff;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.calendar-day:nth-child(7n) {
|
|
441
|
+
border-right: none;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.calendar-day.weekend {
|
|
445
|
+
background: #f8f9fa;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.calendar-day.weekend:hover {
|
|
449
|
+
background: #e9ecef;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.calendar-day.next-month,
|
|
453
|
+
.calendar-day.prev-month {
|
|
454
|
+
background: #f8f9fa;
|
|
455
|
+
opacity: 0.6;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.calendar-day.next-month:hover,
|
|
459
|
+
.calendar-day.prev-month:hover {
|
|
460
|
+
background: #e9ecef;
|
|
461
|
+
opacity: 0.8;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.calendar-day.today {
|
|
465
|
+
background: #f7f8d7;
|
|
466
|
+
font-weight: bold;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
.calendar-day.today:hover {
|
|
470
|
+
background: #bbdefb;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
.day-content {
|
|
475
|
+
height: 100%;
|
|
476
|
+
display: flex;
|
|
477
|
+
flex-direction: column;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
.day-number {
|
|
481
|
+
font-size: 0.9rem;
|
|
482
|
+
margin-bottom: 0.25rem;
|
|
483
|
+
color: #333;
|
|
484
|
+
text-decoration: none;
|
|
485
|
+
display: inline-block;
|
|
486
|
+
padding: 0.1rem;
|
|
487
|
+
border-radius: 4px;
|
|
488
|
+
transition: all 0.2s;
|
|
489
|
+
min-width: 1.5rem;
|
|
490
|
+
text-align: center;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.day-number.clickable {
|
|
494
|
+
cursor: pointer;
|
|
495
|
+
padding: 0.25rem;
|
|
496
|
+
border-radius: 4px;
|
|
497
|
+
transition: background-color 0.2s;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.day-content:hover .day-number.clickable {
|
|
501
|
+
color: #007bff;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.day-events {
|
|
505
|
+
flex: 1;
|
|
506
|
+
overflow: hidden;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.calendar-event {
|
|
510
|
+
background: #d6f1aa;
|
|
511
|
+
border: 1px solid #c4db96;
|
|
512
|
+
margin-bottom: 0.25rem;
|
|
513
|
+
cursor: pointer;
|
|
514
|
+
transition: all 0.2s;
|
|
515
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
516
|
+
padding: 0.1rem 0.1rem;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
.calendar-event:hover {
|
|
520
|
+
background: #b3de6e;
|
|
521
|
+
border-color: #c4db96;
|
|
522
|
+
transform: translateY(-1px);
|
|
523
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
.calendar-event.multi-day-start {
|
|
527
|
+
border-top-left-radius: 4px;
|
|
528
|
+
border-bottom-left-radius: 4px;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
.calendar-event.multi-day-middle {
|
|
532
|
+
border-radius: 0;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
.calendar-event.multi-day-end {
|
|
536
|
+
border-top-right-radius: 4px;
|
|
537
|
+
border-bottom-right-radius: 4px;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.event-content {
|
|
541
|
+
cursor: pointer;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.event-title {
|
|
545
|
+
font-weight: bold;
|
|
546
|
+
font-size: 0.85rem;
|
|
547
|
+
color: #333;
|
|
548
|
+
white-space: normal;
|
|
549
|
+
overflow: hidden;
|
|
550
|
+
text-overflow: ellipsis;
|
|
551
|
+
display: flex;
|
|
552
|
+
align-items: center;
|
|
553
|
+
gap: 0.25rem;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
.multi-day-indicator {
|
|
557
|
+
font-size: 0.7rem;
|
|
558
|
+
color: #007bff;
|
|
559
|
+
font-weight: bold;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
.event-time {
|
|
563
|
+
font-size: 0.75rem;
|
|
564
|
+
color: #666;
|
|
565
|
+
margin-top: 0.125rem;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
.more-events {
|
|
569
|
+
font-size: 0.75rem;
|
|
570
|
+
color: #666;
|
|
571
|
+
text-align: center;
|
|
572
|
+
padding: 0.25rem;
|
|
573
|
+
background: #f8f9fa;
|
|
574
|
+
border-radius: 4px;
|
|
575
|
+
margin-top: 0.25rem;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/* Responsive design */
|
|
579
|
+
@media (max-width: 768px) {
|
|
580
|
+
.calendar-header-nav {
|
|
581
|
+
flex-wrap: wrap;
|
|
582
|
+
gap: 0.5rem;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
.calendar-title {
|
|
586
|
+
font-size: 1.2rem;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
.calendar-day {
|
|
590
|
+
min-height: 80px;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.day-number {
|
|
594
|
+
font-size: 1rem;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
.event-title {
|
|
598
|
+
font-size: 0.8rem;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
.event-time {
|
|
602
|
+
font-size: 0.7rem;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
@media (max-width: 480px) {
|
|
607
|
+
.calendar-grid {
|
|
608
|
+
min-height: 300px;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
.calendar-day {
|
|
612
|
+
min-height: 60px;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
.day-header {
|
|
616
|
+
padding: 0.5rem 0.25rem;
|
|
617
|
+
font-size: 0.8rem;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
@media (min-width: 768px) {
|
|
622
|
+
.day-content {
|
|
623
|
+
padding: 0.45rem;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
.calendar-event {
|
|
627
|
+
border-radius: 4px;
|
|
628
|
+
padding: 0.25rem 0.5rem;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
@media (prefers-color-scheme: dark) {
|
|
633
|
+
.calendar-container {
|
|
634
|
+
background: #565656;
|
|
635
|
+
border-color: #374151;
|
|
636
|
+
border: 1px solid #565656;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
.calendar-day.today {
|
|
640
|
+
background: #646c70;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
.calendar-day.weekend {
|
|
644
|
+
background: #444;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
.calendar-day.weekend:hover {
|
|
648
|
+
background: #1a1a1a;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
.calendar-day.next-month,
|
|
652
|
+
.calendar-day.prev-month {
|
|
653
|
+
background: #1a1a1a !important;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
.calendar-day.next-month:hover,
|
|
657
|
+
.calendar-day.prev-month:hover {
|
|
658
|
+
background: #374151;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
.calendar-day.today:hover {
|
|
662
|
+
background: #374151;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
.calendar-day {
|
|
666
|
+
border: 1px solid #3b3b3b;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
.day-number {
|
|
670
|
+
color: #f9fafb;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
.day-header {
|
|
674
|
+
color: #f9fafb;
|
|
675
|
+
background: #444 !important;
|
|
676
|
+
border: 1px solid #374151;
|
|
677
|
+
border-color: #374151;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
.day-content:hover .day-number.clickable {
|
|
681
|
+
color: #f9fafb;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
.calendar-day:hover {
|
|
685
|
+
background-color: #374151;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
.calendar-day.today:hover {
|
|
689
|
+
background: #374151;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
.calendar-day.weekend:hover {
|
|
693
|
+
background: #374151;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
</style>
|
|
@@ -2,15 +2,28 @@
|
|
|
2
2
|
<div class="pagination">
|
|
3
3
|
<div class="pagination-info">
|
|
4
4
|
<span class="pagination-text">
|
|
5
|
-
|
|
5
|
+
Page size:
|
|
6
|
+
<select
|
|
7
|
+
id="page-size"
|
|
8
|
+
v-model="localPageSize"
|
|
9
|
+
@change="handlePageSizeChange"
|
|
10
|
+
class="page-size-select"
|
|
11
|
+
>
|
|
12
|
+
<option value="10">10</option>
|
|
13
|
+
<option value="25">25</option>
|
|
14
|
+
<option value="50">50</option>
|
|
15
|
+
<option value="100">100</option>
|
|
16
|
+
</select>
|
|
17
|
+
|
|
18
|
+
Showing {{ startItem + 1 }}-{{ endItem }} of {{ total }} {{ itemTitle }}
|
|
6
19
|
</span>
|
|
7
20
|
</div>
|
|
8
21
|
|
|
9
22
|
<div class="pagination-controls">
|
|
10
23
|
<button
|
|
11
|
-
class="
|
|
12
|
-
:disabled="
|
|
13
|
-
@click="goToPage(
|
|
24
|
+
class="button"
|
|
25
|
+
:disabled="currentPageValue === 1"
|
|
26
|
+
@click="goToPage(currentPageValue - 1)"
|
|
14
27
|
title="Previous page"
|
|
15
28
|
>
|
|
16
29
|
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
|
@@ -19,11 +32,10 @@
|
|
|
19
32
|
</button>
|
|
20
33
|
|
|
21
34
|
|
|
22
|
-
<div class="pagination-pages">
|
|
23
35
|
<button
|
|
24
36
|
v-if="showFirstPage"
|
|
25
|
-
class="
|
|
26
|
-
:class="{ active:
|
|
37
|
+
class="button"
|
|
38
|
+
:class="{ active: currentPageValue === 1 }"
|
|
27
39
|
@click="goToPage(1)"
|
|
28
40
|
>
|
|
29
41
|
1
|
|
@@ -34,8 +46,8 @@
|
|
|
34
46
|
<button
|
|
35
47
|
v-for="page in visiblePages"
|
|
36
48
|
:key="page"
|
|
37
|
-
class="
|
|
38
|
-
:class="{ active:
|
|
49
|
+
class="button"
|
|
50
|
+
:class="{ active: currentPageValue === page }"
|
|
39
51
|
@click="goToPage(page)"
|
|
40
52
|
>
|
|
41
53
|
{{ page }}
|
|
@@ -45,18 +57,17 @@
|
|
|
45
57
|
|
|
46
58
|
<button
|
|
47
59
|
v-if="showLastPage"
|
|
48
|
-
class="
|
|
49
|
-
:class="{ active:
|
|
60
|
+
class="button"
|
|
61
|
+
:class="{ active: currentPageValue === totalPages }"
|
|
50
62
|
@click="goToPage(totalPages)"
|
|
51
63
|
>
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
</div>
|
|
64
|
+
{{ totalPages }}
|
|
65
|
+
</button>
|
|
55
66
|
|
|
56
67
|
<button
|
|
57
|
-
class="
|
|
58
|
-
:disabled="
|
|
59
|
-
@click="goToPage(
|
|
68
|
+
class="button"
|
|
69
|
+
:disabled="currentPageValue === totalPages"
|
|
70
|
+
@click="goToPage(currentPageValue + 1)"
|
|
60
71
|
title="Next page"
|
|
61
72
|
>
|
|
62
73
|
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
|
@@ -64,21 +75,6 @@
|
|
|
64
75
|
</svg>
|
|
65
76
|
</button>
|
|
66
77
|
</div>
|
|
67
|
-
|
|
68
|
-
<div class="pagination-size" v-if="canChangePageSize">
|
|
69
|
-
<label for="page-size">Items per page:</label>
|
|
70
|
-
<select
|
|
71
|
-
id="page-size"
|
|
72
|
-
v-model="localPageSize"
|
|
73
|
-
@change="handlePageSizeChange"
|
|
74
|
-
class="page-size-select"
|
|
75
|
-
>
|
|
76
|
-
<option value="10">10</option>
|
|
77
|
-
<option value="25">25</option>
|
|
78
|
-
<option value="50">50</option>
|
|
79
|
-
<option value="100">100</option>
|
|
80
|
-
</select>
|
|
81
|
-
</div>
|
|
82
78
|
</div>
|
|
83
79
|
</template>
|
|
84
80
|
|
|
@@ -86,10 +82,6 @@
|
|
|
86
82
|
import { ref, computed, watch } from 'vue'
|
|
87
83
|
|
|
88
84
|
const props = defineProps({
|
|
89
|
-
pageSize: {
|
|
90
|
-
type: Number,
|
|
91
|
-
default: 25
|
|
92
|
-
},
|
|
93
85
|
total: {
|
|
94
86
|
type: Number,
|
|
95
87
|
required: true
|
|
@@ -100,30 +92,46 @@ const props = defineProps({
|
|
|
100
92
|
},
|
|
101
93
|
canChangePageSize: {
|
|
102
94
|
type: Boolean,
|
|
103
|
-
default:
|
|
95
|
+
default: true,
|
|
104
96
|
},
|
|
105
97
|
itemTitle: {
|
|
106
98
|
type: String,
|
|
107
99
|
default: 'items'
|
|
100
|
+
},
|
|
101
|
+
// Support for v-model binding
|
|
102
|
+
page: {
|
|
103
|
+
type: Number,
|
|
104
|
+
default: 1
|
|
105
|
+
},
|
|
106
|
+
pageSize: {
|
|
107
|
+
type: Number,
|
|
108
|
+
default: 25
|
|
108
109
|
}
|
|
109
110
|
})
|
|
110
111
|
|
|
111
|
-
const emit = defineEmits(['page-change', 'page-size-change'])
|
|
112
|
+
const emit = defineEmits(['page-change', 'page-size-change', 'update:page', 'update:pageSize'])
|
|
112
113
|
|
|
113
114
|
const localPageSize = ref(props.pageSize)
|
|
114
115
|
const localCurrentPage = ref(props.currentPage)
|
|
115
116
|
|
|
117
|
+
// Computed property to get the current page value (supports both v-model and regular props)
|
|
118
|
+
const currentPageValue = computed(() => {
|
|
119
|
+
// When using v-model, the page prop will be reactive and change
|
|
120
|
+
// When using regular props, currentPage will be reactive
|
|
121
|
+
return props.page
|
|
122
|
+
})
|
|
123
|
+
|
|
116
124
|
const totalPages = computed(() => Math.ceil(props.total / localPageSize.value))
|
|
117
125
|
|
|
118
|
-
const startItem = computed(() => (
|
|
119
|
-
const endItem = computed(() => Math.min(
|
|
126
|
+
const startItem = computed(() => (currentPageValue.value - 1) * localPageSize.value)
|
|
127
|
+
const endItem = computed(() => Math.min(currentPageValue.value * localPageSize.value, props.total))
|
|
120
128
|
|
|
121
129
|
const maxVisiblePages = 5
|
|
122
130
|
const visiblePages = computed(() => {
|
|
123
131
|
const pages = []
|
|
124
132
|
const halfVisible = Math.floor(maxVisiblePages / 2)
|
|
125
133
|
|
|
126
|
-
let start = Math.max(1,
|
|
134
|
+
let start = Math.max(1, currentPageValue.value - halfVisible)
|
|
127
135
|
let end = Math.min(totalPages.value, start + maxVisiblePages - 1)
|
|
128
136
|
|
|
129
137
|
if (end - start < maxVisiblePages - 1) {
|
|
@@ -143,22 +151,29 @@ const showFirstEllipsis = computed(() => visiblePages.value[0] > 2)
|
|
|
143
151
|
const showLastEllipsis = computed(() => visiblePages.value[visiblePages.value.length - 1] < totalPages.value - 1)
|
|
144
152
|
|
|
145
153
|
function goToPage(page) {
|
|
146
|
-
if (page >= 1 && page <= totalPages.value && page !==
|
|
154
|
+
if (page >= 1 && page <= totalPages.value && page !== currentPageValue.value) {
|
|
147
155
|
localCurrentPage.value = page
|
|
148
156
|
emit('page-change', page)
|
|
157
|
+
emit('update:page', page)
|
|
149
158
|
}
|
|
150
159
|
}
|
|
151
160
|
|
|
152
161
|
function handlePageSizeChange() {
|
|
153
162
|
localCurrentPage.value = 1
|
|
154
163
|
emit('page-size-change', localPageSize.value)
|
|
164
|
+
emit('update:pageSize', localPageSize.value)
|
|
155
165
|
emit('page-change', 1)
|
|
166
|
+
emit('update:page', 1)
|
|
156
167
|
}
|
|
157
168
|
|
|
158
169
|
watch(() => props.currentPage, (newPage) => {
|
|
159
170
|
localCurrentPage.value = newPage
|
|
160
171
|
})
|
|
161
172
|
|
|
173
|
+
watch(() => props.page, (newPage) => {
|
|
174
|
+
localCurrentPage.value = newPage
|
|
175
|
+
})
|
|
176
|
+
|
|
162
177
|
watch(() => props.pageSize, (newSize) => {
|
|
163
178
|
localPageSize.value = newSize
|
|
164
179
|
})
|
|
@@ -187,71 +202,39 @@ watch(() => props.pageSize, (newSize) => {
|
|
|
187
202
|
gap: 0.5rem;
|
|
188
203
|
}
|
|
189
204
|
|
|
190
|
-
.
|
|
191
|
-
display: flex;
|
|
192
|
-
align-items: center;
|
|
193
|
-
gap: 0.25rem;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
.pagination-btn {
|
|
205
|
+
.button {
|
|
197
206
|
display: flex;
|
|
198
207
|
align-items: center;
|
|
199
208
|
justify-content: center;
|
|
200
209
|
min-width: 2.5rem;
|
|
201
210
|
height: 2.5rem;
|
|
202
|
-
padding: 0.5rem;
|
|
203
|
-
border: 1px solid #dee2e6;
|
|
204
|
-
background: #fff;
|
|
205
|
-
color: #495057;
|
|
206
|
-
text-decoration: none;
|
|
207
|
-
border-radius: 4px;
|
|
208
|
-
cursor: pointer;
|
|
209
|
-
transition: all 0.2s ease;
|
|
210
|
-
font-size: 0.875rem;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
.pagination-btn:hover:not(:disabled) {
|
|
214
|
-
background: #e9ecef;
|
|
215
|
-
border-color: #adb5bd;
|
|
216
|
-
color: #495057;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
.pagination-btn.active {
|
|
220
|
-
background: #c6d0d7;
|
|
221
|
-
color: #333;
|
|
222
211
|
}
|
|
223
212
|
|
|
224
|
-
.
|
|
213
|
+
.button:disabled {
|
|
225
214
|
opacity: 0.5;
|
|
215
|
+
background: transparent;
|
|
226
216
|
cursor: not-allowed;
|
|
227
217
|
}
|
|
228
218
|
|
|
219
|
+
.button.active {
|
|
220
|
+
background: #545f69;
|
|
221
|
+
color: #fff;
|
|
222
|
+
}
|
|
223
|
+
|
|
229
224
|
.pagination-ellipsis {
|
|
230
225
|
padding: 0.5rem;
|
|
231
226
|
color: #6c757d;
|
|
232
227
|
font-size: 0.875rem;
|
|
233
228
|
}
|
|
234
229
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
font-size: 0.875rem;
|
|
240
|
-
color: #6c757d;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
.page-size-select {
|
|
244
|
-
padding: 0.25rem 0.5rem;
|
|
245
|
-
border: 1px solid #dee2e6;
|
|
246
|
-
border-radius: 4px;
|
|
247
|
-
background: #fff;
|
|
248
|
-
font-size: 0.875rem;
|
|
230
|
+
#page-size {
|
|
231
|
+
background: transparent;
|
|
232
|
+
margin-left: 0.5rem;
|
|
233
|
+
margin-right: 0.5rem;
|
|
249
234
|
}
|
|
250
235
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
border-color: #5681af;
|
|
254
|
-
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
|
236
|
+
option {
|
|
237
|
+
background: #545f69;
|
|
255
238
|
}
|
|
256
239
|
|
|
257
240
|
/* Responsive design */
|
package/vue/views/App.vue
CHANGED
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
onMounted(() => {
|
|
46
46
|
sidebar.value.addRouterLink('Welcome')
|
|
47
47
|
sidebar.value.addRouterLink('TableExample')
|
|
48
|
+
sidebar.value.addRouterLink('CalendarExample')
|
|
48
49
|
sidebar.value.addSeparator('separator-1');
|
|
49
50
|
sidebar.value.addRouterLink('ViewItem', { id: 1 })
|
|
50
51
|
sidebar.value.addSeparator('separator-2');
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Section
|
|
3
|
+
title="Example Calendar"
|
|
4
|
+
padding
|
|
5
|
+
>
|
|
6
|
+
<template #toolbar>
|
|
7
|
+
<button @click="goToToday">Today</button>
|
|
8
|
+
<button @click="previousMonth">‹</button>
|
|
9
|
+
<div class="date-picker-container">
|
|
10
|
+
<input
|
|
11
|
+
id="goto-date"
|
|
12
|
+
type="month"
|
|
13
|
+
v-model="selectedDate"
|
|
14
|
+
@change="goToSelectedDate"
|
|
15
|
+
class="date-picker-input"
|
|
16
|
+
/>
|
|
17
|
+
</div>
|
|
18
|
+
<button @click="nextMonth">›</button>
|
|
19
|
+
</template>
|
|
20
|
+
<Calendar
|
|
21
|
+
:section="true"
|
|
22
|
+
:events="events"
|
|
23
|
+
:loading="loading"
|
|
24
|
+
:error="error"
|
|
25
|
+
:show-navigation="false"
|
|
26
|
+
:current-month="currentMonthIndex"
|
|
27
|
+
:current-year="currentYear"
|
|
28
|
+
@event-click="handleEventClick"
|
|
29
|
+
@date-click="handleDateClick"
|
|
30
|
+
@month-change="handleMonthChange"
|
|
31
|
+
/>
|
|
32
|
+
</Section>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<script setup>
|
|
36
|
+
import { ref, onMounted } from 'vue'
|
|
37
|
+
import Calendar from '../components/Calendar.vue'
|
|
38
|
+
import Section from '../components/Section.vue'
|
|
39
|
+
|
|
40
|
+
const events = ref([])
|
|
41
|
+
const loading = ref(false)
|
|
42
|
+
const error = ref(null)
|
|
43
|
+
const currentMonth = ref('?')
|
|
44
|
+
const currentMonthIndex = ref(new Date().getMonth())
|
|
45
|
+
const currentYear = ref(new Date().getFullYear())
|
|
46
|
+
|
|
47
|
+
// Date picker state - using native HTML5 month input
|
|
48
|
+
const selectedDate = ref('')
|
|
49
|
+
|
|
50
|
+
// Generate dummy events for the current month
|
|
51
|
+
function generateDummyEvents() {
|
|
52
|
+
const now = new Date()
|
|
53
|
+
const currentMonth = now.getMonth()
|
|
54
|
+
const currentYear = now.getFullYear()
|
|
55
|
+
|
|
56
|
+
// Get the number of days in the current month
|
|
57
|
+
const daysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate()
|
|
58
|
+
|
|
59
|
+
const dummyEvents = []
|
|
60
|
+
|
|
61
|
+
// Add some recurring events
|
|
62
|
+
for (let day = 1; day <= daysInMonth; day++) {
|
|
63
|
+
const date = new Date(currentYear, currentMonth, day)
|
|
64
|
+
const dayOfWeek = date.getDay()
|
|
65
|
+
|
|
66
|
+
// Add weekend events (Saturday and Sunday)
|
|
67
|
+
if (dayOfWeek === 0 || dayOfWeek === 6) {
|
|
68
|
+
dummyEvents.push({
|
|
69
|
+
id: `weekend-${day}`,
|
|
70
|
+
title: 'Weekend Activity',
|
|
71
|
+
date: new Date(currentYear, currentMonth, day, 10, 0),
|
|
72
|
+
description: 'Relaxing weekend activity'
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Add some random events
|
|
77
|
+
if (Math.random() > 0.7) {
|
|
78
|
+
const eventTypes = [
|
|
79
|
+
{ title: 'Team Meeting', time: [9, 0], duration: 1 },
|
|
80
|
+
{ title: 'Client Call', time: [14, 30], duration: 0.5 },
|
|
81
|
+
{ title: 'Project Review', time: [16, 0], duration: 2 },
|
|
82
|
+
{ title: 'Lunch Break', time: [12, 0], duration: 1 },
|
|
83
|
+
{ title: 'Code Review', time: [10, 0], duration: 1.5 }
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
const eventType = eventTypes[Math.floor(Math.random() * eventTypes.length)]
|
|
87
|
+
const [hours, minutes] = eventType.time
|
|
88
|
+
|
|
89
|
+
dummyEvents.push({
|
|
90
|
+
id: `event-${day}-${Math.random().toString(36).substr(2, 9)}`,
|
|
91
|
+
title: eventType.title,
|
|
92
|
+
date: new Date(currentYear, currentMonth, day, hours, minutes),
|
|
93
|
+
description: `Scheduled ${eventType.title.toLowerCase()}`
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Add some multi-day events
|
|
99
|
+
dummyEvents.push({
|
|
100
|
+
id: 'conference-1',
|
|
101
|
+
title: 'Tech Conference',
|
|
102
|
+
startDate: new Date(currentYear, currentMonth, 5, 9, 0),
|
|
103
|
+
endDate: new Date(currentYear, currentMonth, 7, 17, 0),
|
|
104
|
+
description: 'Annual technology conference'
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
dummyEvents.push({
|
|
108
|
+
id: 'vacation-1',
|
|
109
|
+
title: 'Vacation',
|
|
110
|
+
startDate: new Date(currentYear, currentMonth, 15, 0, 0),
|
|
111
|
+
endDate: new Date(currentYear, currentMonth, 18, 23, 59),
|
|
112
|
+
description: 'Family vacation time'
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// Add some all-day events
|
|
116
|
+
dummyEvents.push({
|
|
117
|
+
id: 'holiday-1',
|
|
118
|
+
title: 'Public Holiday',
|
|
119
|
+
date: new Date(currentYear, currentMonth, 1, 0, 0),
|
|
120
|
+
description: 'National holiday - office closed'
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
if (daysInMonth >= 25) {
|
|
124
|
+
dummyEvents.push({
|
|
125
|
+
id: 'deadline-1',
|
|
126
|
+
title: 'Project Deadline',
|
|
127
|
+
date: new Date(currentYear, currentMonth, 25, 0, 0),
|
|
128
|
+
description: 'Important project deadline'
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return dummyEvents
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function handleEventClick(event) {
|
|
136
|
+
console.log('Event clicked:', event)
|
|
137
|
+
alert(`Event: ${event.title}\nDescription: ${event.description || 'No description'}`)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function handleDateClick(date) {
|
|
141
|
+
console.log('Date clicked:', date)
|
|
142
|
+
alert(`Date selected: ${date.toLocaleDateString()}`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Helper function to update the date picker value
|
|
146
|
+
function updateDatePicker(monthIndex, year) {
|
|
147
|
+
const month = String(monthIndex + 1).padStart(2, '0')
|
|
148
|
+
selectedDate.value = `${year}-${month}`
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Navigation functions - using the same fast path as internal calendar navigation
|
|
152
|
+
function previousMonth() {
|
|
153
|
+
const newMonth = currentMonthIndex.value === 0 ? 11 : currentMonthIndex.value - 1
|
|
154
|
+
const newYear = currentMonthIndex.value === 0 ? currentYear.value - 1 : currentYear.value
|
|
155
|
+
|
|
156
|
+
// Update state directly (fast path)
|
|
157
|
+
currentMonthIndex.value = newMonth
|
|
158
|
+
currentYear.value = newYear
|
|
159
|
+
|
|
160
|
+
// Update title
|
|
161
|
+
const date = new Date(newYear, newMonth, 1)
|
|
162
|
+
currentMonth.value = date.toLocaleDateString('en-US', {
|
|
163
|
+
month: 'long',
|
|
164
|
+
year: 'numeric'
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
// Update date picker
|
|
168
|
+
updateDatePicker(newMonth, newYear)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function nextMonth() {
|
|
172
|
+
const newMonth = currentMonthIndex.value === 11 ? 0 : currentMonthIndex.value + 1
|
|
173
|
+
const newYear = currentMonthIndex.value === 11 ? currentYear.value + 1 : currentYear.value
|
|
174
|
+
|
|
175
|
+
// Update state directly (fast path)
|
|
176
|
+
currentMonthIndex.value = newMonth
|
|
177
|
+
currentYear.value = newYear
|
|
178
|
+
|
|
179
|
+
// Update title
|
|
180
|
+
const date = new Date(newYear, newMonth, 1)
|
|
181
|
+
currentMonth.value = date.toLocaleDateString('en-US', {
|
|
182
|
+
month: 'long',
|
|
183
|
+
year: 'numeric'
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// Update date picker
|
|
187
|
+
updateDatePicker(newMonth, newYear)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function goToToday() {
|
|
191
|
+
const now = new Date()
|
|
192
|
+
const newMonth = now.getMonth()
|
|
193
|
+
const newYear = now.getFullYear()
|
|
194
|
+
|
|
195
|
+
// Update state directly (fast path)
|
|
196
|
+
currentMonthIndex.value = newMonth
|
|
197
|
+
currentYear.value = newYear
|
|
198
|
+
|
|
199
|
+
// Update title
|
|
200
|
+
currentMonth.value = now.toLocaleDateString('en-US', {
|
|
201
|
+
month: 'long',
|
|
202
|
+
year: 'numeric'
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
// Update date picker
|
|
206
|
+
updateDatePicker(newMonth, newYear)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function goToSelectedDate() {
|
|
210
|
+
if (!selectedDate.value) return
|
|
211
|
+
|
|
212
|
+
// Parse the YYYY-MM format from the native date picker
|
|
213
|
+
const [year, month] = selectedDate.value.split('-').map(Number)
|
|
214
|
+
const monthIndex = month - 1 // Convert to 0-based month index
|
|
215
|
+
|
|
216
|
+
// Update state directly (fast path)
|
|
217
|
+
currentMonthIndex.value = monthIndex
|
|
218
|
+
currentYear.value = year
|
|
219
|
+
|
|
220
|
+
// Update title
|
|
221
|
+
const date = new Date(year, monthIndex, 1)
|
|
222
|
+
currentMonth.value = date.toLocaleDateString('en-US', {
|
|
223
|
+
month: 'long',
|
|
224
|
+
year: 'numeric'
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function handleMonthChange(month, year) {
|
|
229
|
+
console.log('Month changed:', month, year)
|
|
230
|
+
// In a real app, you might fetch events for the new month
|
|
231
|
+
loading.value = true
|
|
232
|
+
|
|
233
|
+
// Update current month and year state
|
|
234
|
+
currentMonthIndex.value = month
|
|
235
|
+
currentYear.value = year
|
|
236
|
+
|
|
237
|
+
// Use native JavaScript to format month and year
|
|
238
|
+
const date = new Date(year, month, 1)
|
|
239
|
+
currentMonth.value = date.toLocaleDateString('en-US', {
|
|
240
|
+
month: 'long',
|
|
241
|
+
year: 'numeric'
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
// Update date picker to reflect the new month
|
|
245
|
+
updateDatePicker(month, year)
|
|
246
|
+
|
|
247
|
+
setTimeout(() => {
|
|
248
|
+
// Simulate loading delay
|
|
249
|
+
events.value = generateDummyEvents()
|
|
250
|
+
loading.value = false
|
|
251
|
+
}, 500)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
onMounted(() => {
|
|
255
|
+
// Generate initial events for current month
|
|
256
|
+
events.value = generateDummyEvents()
|
|
257
|
+
|
|
258
|
+
// Set initial month title using native JavaScript
|
|
259
|
+
const now = new Date()
|
|
260
|
+
currentMonthIndex.value = now.getMonth()
|
|
261
|
+
currentYear.value = now.getFullYear()
|
|
262
|
+
currentMonth.value = now.toLocaleDateString('en-US', {
|
|
263
|
+
month: 'long',
|
|
264
|
+
year: 'numeric'
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
// Initialize the native date picker with current month/year
|
|
268
|
+
const year = now.getFullYear()
|
|
269
|
+
const month = String(now.getMonth() + 1).padStart(2, '0')
|
|
270
|
+
selectedDate.value = `${year}-${month}`
|
|
271
|
+
})
|
|
272
|
+
</script>
|
|
273
|
+
|
|
274
|
+
<style scoped>
|
|
275
|
+
.date-picker-container {
|
|
276
|
+
display: flex;
|
|
277
|
+
align-items: center;
|
|
278
|
+
gap: 0.5rem;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.date-picker-label {
|
|
282
|
+
font-size: 0.875rem;
|
|
283
|
+
font-weight: 500;
|
|
284
|
+
color: #374151;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.date-picker-input {
|
|
288
|
+
padding: 0.5rem;
|
|
289
|
+
border: 1px solid #d1d5db;
|
|
290
|
+
border-radius: 4px;
|
|
291
|
+
font-size: 0.875rem;
|
|
292
|
+
background: white;
|
|
293
|
+
cursor: pointer;
|
|
294
|
+
transition: all 0.2s;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.date-picker-input:hover {
|
|
298
|
+
border-color: #9ca3af;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.date-picker-input:focus {
|
|
302
|
+
outline: none;
|
|
303
|
+
border-color: #3b82f6;
|
|
304
|
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/* Dark theme support */
|
|
308
|
+
@media (prefers-color-scheme: dark) {
|
|
309
|
+
.date-picker-label {
|
|
310
|
+
color: #d1d5db;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.date-picker-input {
|
|
314
|
+
background: #374151;
|
|
315
|
+
border-color: #4b5563;
|
|
316
|
+
color: #f9fafb;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.date-picker-input:hover {
|
|
320
|
+
border-color: #6b7280;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.date-picker-input:focus {
|
|
324
|
+
border-color: #60a5fa;
|
|
325
|
+
box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.1);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
</style>
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
<Table
|
|
12
12
|
:data = "data"
|
|
13
13
|
:headers = "headers"
|
|
14
|
+
:show-pagination = "true"
|
|
14
15
|
>
|
|
15
16
|
<template #cell-name="{ row, value }">
|
|
16
17
|
<router-link :to="{ name: 'ViewItem', params: { id: row.name } }">
|
|
@@ -59,7 +60,61 @@
|
|
|
59
60
|
{ name: "Quinn", age: 23, city: "Indianapolis" },
|
|
60
61
|
{ name: "Rita", age: 37, city: "Seattle" },
|
|
61
62
|
{ name: "Sam", age: 39, city: "Denver" },
|
|
62
|
-
{ name: "Tina", age: 21, city: "Washington D.C." }
|
|
63
|
+
{ name: "Tina", age: 21, city: "Washington D.C." },
|
|
64
|
+
{ name: "Uma", age: 28, city: "Miami" },
|
|
65
|
+
{ name: "Victor", age: 30, city: "Portland" },
|
|
66
|
+
{ name: "Wendy", age: 29, city: "Nashville" },
|
|
67
|
+
{ name: "Xavier", age: 31, city: "Oklahoma City" },
|
|
68
|
+
{ name: "Yara", age: 27, city: "Tampa" },
|
|
69
|
+
{ name: "Zane", age: 32, city: "Albuquerque" },
|
|
70
|
+
{ name: "Ava", age: 26, city: "Las Vegas" },
|
|
71
|
+
{ name: "Ben", age: 33, city: "New Orleans" },
|
|
72
|
+
{ name: "Cara", age: 25, city: "San Francisco" },
|
|
73
|
+
{ name: "Drew", age: 34, city: "Seattle" },
|
|
74
|
+
{ name: "Ethan", age: 28, city: "Denver" },
|
|
75
|
+
{ name: "Fiona", age: 30, city: "Washington D.C." },
|
|
76
|
+
{ name: "George", age: 29, city: "Miami" },
|
|
77
|
+
{ name: "Hannah", age: 32, city: "San Diego" },
|
|
78
|
+
{ name: "Ian", age: 27, city: "Dallas" },
|
|
79
|
+
{ name: "Jack", age: 31, city: "San Jose" },
|
|
80
|
+
{ name: "Kathy", age: 26, city: "Austin" },
|
|
81
|
+
{ name: "Leo", age: 33, city: "Jacksonville" },
|
|
82
|
+
{ name: "Mia", age: 24, city: "Fort Worth" },
|
|
83
|
+
{ name: "Nina", age: 38, city: "Columbus" },
|
|
84
|
+
{ name: "Oscar", age: 36, city: "Charlotte" },
|
|
85
|
+
{ name: "Paul", age: 34, city: "San Francisco" },
|
|
86
|
+
{ name: "Quinn", age: 23, city: "Indianapolis" },
|
|
87
|
+
{ name: "Rita", age: 37, city: "Seattle" },
|
|
88
|
+
{ name: "Sam", age: 39, city: "Denver" },
|
|
89
|
+
{ name: "Tina", age: 21, city: "Washington D.C." },
|
|
90
|
+
{ name: "Uma", age: 28, city: "Miami" },
|
|
91
|
+
{ name: "Victor", age: 30, city: "Portland" },
|
|
92
|
+
{ name: "Wendy", age: 29, city: "Nashville" },
|
|
93
|
+
{ name: "Xavier", age: 31, city: "Oklahoma City" },
|
|
94
|
+
{ name: "Yara", age: 27, city: "Tampa" },
|
|
95
|
+
{ name: "Zane", age: 32, city: "Albuquerque" },
|
|
96
|
+
{ name: "Ava", age: 26, city: "Las Vegas" },
|
|
97
|
+
{ name: "Ben", age: 33, city: "New Orleans" },
|
|
98
|
+
{ name: "Cara", age: 25, city: "San Francisco" },
|
|
99
|
+
{ name: "Drew", age: 34, city: "Seattle" },
|
|
100
|
+
{ name: "Ethan", age: 28, city: "Denver" },
|
|
101
|
+
{ name: "Fiona", age: 30, city: "Washington D.C." },
|
|
102
|
+
{ name: "George", age: 29, city: "Miami" },
|
|
103
|
+
{ name: "Hannah", age: 32, city: "San Diego" },
|
|
104
|
+
{ name: "Ian", age: 27, city: "Dallas" },
|
|
105
|
+
{ name: "Jack", age: 31, city: "San Jose" },
|
|
106
|
+
{ name: "Kathy", age: 26, city: "Austin" },
|
|
107
|
+
{ name: "Leo", age: 33, city: "Jacksonville" },
|
|
108
|
+
{ name: "Mia", age: 24, city: "Fort Worth" },
|
|
109
|
+
{ name: "Nina", age: 38, city: "Columbus" },
|
|
110
|
+
{ name: "Oscar", age: 36, city: "Charlotte" },
|
|
111
|
+
{ name: "Paul", age: 34, city: "San Francisco" },
|
|
112
|
+
{ name: "Quinn", age: 23, city: "Indianapolis" },
|
|
113
|
+
{ name: "Rita", age: 37, city: "Seattle" },
|
|
114
|
+
{ name: "Sam", age: 39, city: "Denver" },
|
|
115
|
+
{ name: "Tina", age: 21, city: "Washington D.C." },
|
|
116
|
+
{ name: "Uma", age: 28, city: "Miami" },
|
|
117
|
+
{ name: "Victor", age: 30, city: "Portland" },
|
|
63
118
|
]
|
|
64
119
|
})
|
|
65
120
|
</script>
|