@eeplatform/nuxt-layer-common 1.2.10 → 1.3.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/CHANGELOG.md +12 -0
- package/components/Calendar.vue +199 -0
- package/components/CalendarDay.vue +531 -0
- package/components/CalendarMonth.vue +452 -0
- package/components/CalendarWeek.vue +545 -0
- package/components/CalendarYear.vue +295 -0
- package/components/OfficeForm.vue +194 -0
- package/components/OfficeMain.vue +126 -0
- package/components/SchoolFormUpload.vue +7 -50
- package/components/SchoolMain.vue +3 -3
- package/composables/useOffice.ts +40 -0
- package/composables/usePlantilla.ts +52 -0
- package/package.json +1 -1
- package/plugins/API.ts +12 -0
- package/types/office.d.ts +12 -0
- package/types/plantilla.d.ts +29 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="calendar month-view" :style="{ height: calendarHeight }">
|
|
3
|
+
<div v-for="day in weekdays" :key="day" class="header border">
|
|
4
|
+
<slot name="header" :day="day" :dayIndex="weekdays.indexOf(day)">
|
|
5
|
+
{{ day }}
|
|
6
|
+
</slot>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<div
|
|
10
|
+
v-for="cell in calendarCells"
|
|
11
|
+
:key="cell.type + '-' + cell.day"
|
|
12
|
+
class="day border"
|
|
13
|
+
:class="{
|
|
14
|
+
today: cell.type === 'current' && isToday(cell.day),
|
|
15
|
+
'other-month': cell.type !== 'current',
|
|
16
|
+
}"
|
|
17
|
+
@click="onDayClick(cell)"
|
|
18
|
+
>
|
|
19
|
+
<slot
|
|
20
|
+
name="day"
|
|
21
|
+
:cell="cell"
|
|
22
|
+
:day="cell.day"
|
|
23
|
+
:date="cell.date"
|
|
24
|
+
:type="cell.type"
|
|
25
|
+
:monthAbbr="cell.monthAbbr"
|
|
26
|
+
:showMonthAbbr="cell.showMonthAbbr"
|
|
27
|
+
:isToday="cell.type === 'current' && isToday(cell.day)"
|
|
28
|
+
:isCurrentMonth="cell.type === 'current'"
|
|
29
|
+
:isPrevMonth="cell.type === 'prev'"
|
|
30
|
+
:isNextMonth="cell.type === 'next'"
|
|
31
|
+
:events="cell.events"
|
|
32
|
+
:eventPositions="cell.eventPositions"
|
|
33
|
+
:hasEvents="cell.events.length > 0"
|
|
34
|
+
>
|
|
35
|
+
<!-- Default slot content (fallback) -->
|
|
36
|
+
<div class="day-content">
|
|
37
|
+
<div class="day-number">{{ cell.day }}</div>
|
|
38
|
+
<div v-if="cell.showMonthAbbr" class="month-abbr">
|
|
39
|
+
{{ cell.monthAbbr }}
|
|
40
|
+
</div>
|
|
41
|
+
<!-- Default event indicators - Google Calendar style -->
|
|
42
|
+
<div v-if="cell.events.length > 0" class="event-indicators">
|
|
43
|
+
<div
|
|
44
|
+
v-for="(event, index) in cell.events.slice(0, 4)"
|
|
45
|
+
:key="index"
|
|
46
|
+
class="default-event-block"
|
|
47
|
+
:style="{ backgroundColor: event.color || '#2196f3' }"
|
|
48
|
+
:title="getEventTooltip(event)"
|
|
49
|
+
>
|
|
50
|
+
<div class="event-content">
|
|
51
|
+
<span class="default-event-text">{{ event.title }}</span>
|
|
52
|
+
<span
|
|
53
|
+
v-if="event.startTime || event.endTime"
|
|
54
|
+
class="event-time"
|
|
55
|
+
>
|
|
56
|
+
{{ formatEventTime(event) }}
|
|
57
|
+
</span>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
<div v-if="cell.events.length > 4" class="more-events">
|
|
61
|
+
+{{ cell.events.length - 4 }} more
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</slot>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</template>
|
|
69
|
+
|
|
70
|
+
<script setup>
|
|
71
|
+
import { computed } from "vue";
|
|
72
|
+
|
|
73
|
+
const props = defineProps({
|
|
74
|
+
current: {
|
|
75
|
+
type: Date,
|
|
76
|
+
required: true,
|
|
77
|
+
},
|
|
78
|
+
events: {
|
|
79
|
+
type: Array,
|
|
80
|
+
default: () => [],
|
|
81
|
+
},
|
|
82
|
+
calendarHeight: {
|
|
83
|
+
type: String,
|
|
84
|
+
default: "calc(100vh - 100px)",
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const emit = defineEmits(["dayClick"]);
|
|
89
|
+
|
|
90
|
+
const weekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
91
|
+
const monthNames = [
|
|
92
|
+
"Jan",
|
|
93
|
+
"Feb",
|
|
94
|
+
"Mar",
|
|
95
|
+
"Apr",
|
|
96
|
+
"May",
|
|
97
|
+
"Jun",
|
|
98
|
+
"Jul",
|
|
99
|
+
"Aug",
|
|
100
|
+
"Sep",
|
|
101
|
+
"Oct",
|
|
102
|
+
"Nov",
|
|
103
|
+
"Dec",
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
// Days in current month
|
|
107
|
+
const daysInMonth = computed(() => {
|
|
108
|
+
const year = props.current.getFullYear();
|
|
109
|
+
const month = props.current.getMonth();
|
|
110
|
+
return new Date(year, month + 1, 0).getDate();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// First day index of current month (0=Sun)
|
|
114
|
+
const firstDayOfMonth = computed(() => {
|
|
115
|
+
const year = props.current.getFullYear();
|
|
116
|
+
const month = props.current.getMonth();
|
|
117
|
+
return new Date(year, month, 1).getDay();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Days in previous month
|
|
121
|
+
const prevMonthDays = computed(() => {
|
|
122
|
+
const year = props.current.getFullYear();
|
|
123
|
+
const month = props.current.getMonth();
|
|
124
|
+
return new Date(year, month, 0).getDate();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Total cells in calendar (6 rows x 7 columns)
|
|
128
|
+
const totalCells = 42;
|
|
129
|
+
|
|
130
|
+
// Helper functions for event calculations
|
|
131
|
+
const isSameDay = (date1, date2) => {
|
|
132
|
+
return date1.toDateString() === date2.toDateString();
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const isDateInRange = (date, startDate, endDate) => {
|
|
136
|
+
const checkDate = new Date(date);
|
|
137
|
+
checkDate.setHours(0, 0, 0, 0);
|
|
138
|
+
const start = new Date(startDate);
|
|
139
|
+
start.setHours(0, 0, 0, 0);
|
|
140
|
+
const end = new Date(endDate);
|
|
141
|
+
end.setHours(0, 0, 0, 0);
|
|
142
|
+
|
|
143
|
+
return checkDate >= start && checkDate <= end;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const getEventsForDate = (date) => {
|
|
147
|
+
return props.events.filter((event) =>
|
|
148
|
+
isDateInRange(date, event.startDate, event.endDate)
|
|
149
|
+
);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const getEventPosition = (event, date) => {
|
|
153
|
+
const isStart = isSameDay(date, event.startDate);
|
|
154
|
+
const isEnd = isSameDay(date, event.endDate);
|
|
155
|
+
const isMiddle =
|
|
156
|
+
!isStart && !isEnd && isDateInRange(date, event.startDate, event.endDate);
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
isStart,
|
|
160
|
+
isEnd,
|
|
161
|
+
isMiddle,
|
|
162
|
+
isSingle: isStart && isEnd, // single day event
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Build calendar with prev, current, next month
|
|
167
|
+
const calendarCells = computed(() => {
|
|
168
|
+
const cells = [];
|
|
169
|
+
const year = props.current.getFullYear();
|
|
170
|
+
const month = props.current.getMonth();
|
|
171
|
+
|
|
172
|
+
// Previous month dates (for empty spots at start)
|
|
173
|
+
const prevMonth = month === 0 ? 11 : month - 1;
|
|
174
|
+
const prevYear = month === 0 ? year - 1 : year;
|
|
175
|
+
const prevMonthDaysArray = [];
|
|
176
|
+
for (let i = firstDayOfMonth.value - 1; i >= 0; i--) {
|
|
177
|
+
prevMonthDaysArray.push(prevMonthDays.value - i);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
prevMonthDaysArray.forEach((day, index) => {
|
|
181
|
+
const cellDate = new Date(prevYear, prevMonth, day);
|
|
182
|
+
const eventsForDate = getEventsForDate(cellDate);
|
|
183
|
+
|
|
184
|
+
cells.push({
|
|
185
|
+
day: day,
|
|
186
|
+
type: "prev",
|
|
187
|
+
monthAbbr: monthNames[prevMonth],
|
|
188
|
+
showMonthAbbr: index === prevMonthDaysArray.length - 1,
|
|
189
|
+
date: cellDate,
|
|
190
|
+
events: eventsForDate,
|
|
191
|
+
eventPositions: eventsForDate.map((event) =>
|
|
192
|
+
getEventPosition(event, cellDate)
|
|
193
|
+
),
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Current month dates
|
|
198
|
+
for (let i = 1; i <= daysInMonth.value; i++) {
|
|
199
|
+
const cellDate = new Date(year, month, i);
|
|
200
|
+
const eventsForDate = getEventsForDate(cellDate);
|
|
201
|
+
|
|
202
|
+
cells.push({
|
|
203
|
+
day: i,
|
|
204
|
+
type: "current",
|
|
205
|
+
monthAbbr: monthNames[month],
|
|
206
|
+
showMonthAbbr: false,
|
|
207
|
+
date: cellDate,
|
|
208
|
+
events: eventsForDate,
|
|
209
|
+
eventPositions: eventsForDate.map((event) =>
|
|
210
|
+
getEventPosition(event, cellDate)
|
|
211
|
+
),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Next month dates (to fill remaining cells)
|
|
216
|
+
const nextMonth = month === 11 ? 0 : month + 1;
|
|
217
|
+
const nextYear = month === 11 ? year + 1 : year;
|
|
218
|
+
let nextDays = totalCells - cells.length;
|
|
219
|
+
for (let i = 1; i <= nextDays; i++) {
|
|
220
|
+
const cellDate = new Date(nextYear, nextMonth, i);
|
|
221
|
+
const eventsForDate = getEventsForDate(cellDate);
|
|
222
|
+
|
|
223
|
+
cells.push({
|
|
224
|
+
day: i,
|
|
225
|
+
type: "next",
|
|
226
|
+
monthAbbr: monthNames[nextMonth],
|
|
227
|
+
showMonthAbbr: i === 1,
|
|
228
|
+
date: cellDate,
|
|
229
|
+
events: eventsForDate,
|
|
230
|
+
eventPositions: eventsForDate.map((event) =>
|
|
231
|
+
getEventPosition(event, cellDate)
|
|
232
|
+
),
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return cells;
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Check if a day is today
|
|
240
|
+
const isToday = (day) => {
|
|
241
|
+
const today = new Date();
|
|
242
|
+
return (
|
|
243
|
+
today.getDate() === day &&
|
|
244
|
+
today.getMonth() === props.current.getMonth() &&
|
|
245
|
+
today.getFullYear() === props.current.getFullYear()
|
|
246
|
+
);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// Format event time display
|
|
250
|
+
const formatEventTime = (event) => {
|
|
251
|
+
if (!event.startTime && !event.endTime) return "";
|
|
252
|
+
|
|
253
|
+
const formatTime = (timeStr) => {
|
|
254
|
+
if (!timeStr) return "";
|
|
255
|
+
|
|
256
|
+
// If it's already a formatted 12-hour string (contains AM/PM), return it as-is
|
|
257
|
+
if (typeof timeStr === "string" && /AM|PM/i.test(timeStr)) {
|
|
258
|
+
return timeStr;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// If it's a 24-hour format string, convert to 12-hour
|
|
262
|
+
if (typeof timeStr === "string" && /^\d{1,2}:\d{2}$/.test(timeStr)) {
|
|
263
|
+
const [hours, minutes] = timeStr.split(":");
|
|
264
|
+
const hour24 = parseInt(hours);
|
|
265
|
+
const hour12 = hour24 === 0 ? 12 : hour24 > 12 ? hour24 - 12 : hour24;
|
|
266
|
+
const period = hour24 >= 12 ? "PM" : "AM";
|
|
267
|
+
return `${hour12}:${minutes} ${period}`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// If it's a Date object, format it to 12-hour
|
|
271
|
+
if (timeStr instanceof Date) {
|
|
272
|
+
return timeStr.toLocaleTimeString([], {
|
|
273
|
+
hour: "numeric",
|
|
274
|
+
minute: "2-digit",
|
|
275
|
+
hour12: true,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return timeStr;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const start = formatTime(event.startTime);
|
|
283
|
+
const end = formatTime(event.endTime);
|
|
284
|
+
|
|
285
|
+
if (start && end) {
|
|
286
|
+
return `${start} - ${end}`;
|
|
287
|
+
} else if (start) {
|
|
288
|
+
return start;
|
|
289
|
+
} else if (end) {
|
|
290
|
+
return `Until ${end}`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return "";
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
// Get tooltip text for event
|
|
297
|
+
const getEventTooltip = (event) => {
|
|
298
|
+
let tooltip = event.title;
|
|
299
|
+
|
|
300
|
+
if (event.type) {
|
|
301
|
+
tooltip += ` (${event.type})`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const timeStr = formatEventTime(event);
|
|
305
|
+
if (timeStr) {
|
|
306
|
+
tooltip += `\n${timeStr}`;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (event.description) {
|
|
310
|
+
tooltip += `\n${event.description}`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return tooltip;
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// Handle day click
|
|
317
|
+
const onDayClick = (cell) => {
|
|
318
|
+
emit("dayClick", {
|
|
319
|
+
date: cell.date,
|
|
320
|
+
day: cell.day,
|
|
321
|
+
type: cell.type,
|
|
322
|
+
monthAbbr: cell.monthAbbr,
|
|
323
|
+
});
|
|
324
|
+
};
|
|
325
|
+
</script>
|
|
326
|
+
|
|
327
|
+
<style scoped>
|
|
328
|
+
.calendar {
|
|
329
|
+
display: grid;
|
|
330
|
+
grid-template-columns: repeat(7, 1fr);
|
|
331
|
+
grid-template-rows: auto repeat(6, 1fr);
|
|
332
|
+
width: 100%;
|
|
333
|
+
font-family: sans-serif;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.header {
|
|
337
|
+
font-weight: bold;
|
|
338
|
+
text-align: center;
|
|
339
|
+
padding: 10px 0;
|
|
340
|
+
background-color: #f0f0f0;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.day {
|
|
344
|
+
display: flex;
|
|
345
|
+
flex-direction: column;
|
|
346
|
+
justify-content: flex-start;
|
|
347
|
+
align-items: center;
|
|
348
|
+
text-align: center;
|
|
349
|
+
cursor: pointer;
|
|
350
|
+
transition: background-color 0.2s;
|
|
351
|
+
position: relative;
|
|
352
|
+
padding: 4px 2px;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.day-content {
|
|
356
|
+
width: 100%;
|
|
357
|
+
height: 100%;
|
|
358
|
+
display: flex;
|
|
359
|
+
flex-direction: column;
|
|
360
|
+
align-items: center;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.day-number {
|
|
364
|
+
font-size: 1.1em;
|
|
365
|
+
font-weight: 500;
|
|
366
|
+
margin-bottom: 2px;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.month-abbr {
|
|
370
|
+
font-size: 0.7em;
|
|
371
|
+
color: #888;
|
|
372
|
+
margin-top: 2px;
|
|
373
|
+
font-weight: normal;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.event-indicators {
|
|
377
|
+
width: 100%;
|
|
378
|
+
margin-top: auto;
|
|
379
|
+
padding-bottom: 2px;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.default-event-block {
|
|
383
|
+
margin: 1px 0;
|
|
384
|
+
border-radius: 4px;
|
|
385
|
+
font-size: 0.7em;
|
|
386
|
+
display: flex;
|
|
387
|
+
align-items: flex-start;
|
|
388
|
+
min-height: 18px;
|
|
389
|
+
padding: 2px 4px;
|
|
390
|
+
box-sizing: border-box;
|
|
391
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
|
|
392
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
393
|
+
overflow: hidden;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.event-content {
|
|
397
|
+
display: flex;
|
|
398
|
+
flex-direction: column;
|
|
399
|
+
width: 100%;
|
|
400
|
+
line-height: 1.1;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.default-event-text {
|
|
404
|
+
color: white;
|
|
405
|
+
font-weight: 500;
|
|
406
|
+
font-size: 0.7em;
|
|
407
|
+
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
|
|
408
|
+
white-space: nowrap;
|
|
409
|
+
overflow: hidden;
|
|
410
|
+
text-overflow: ellipsis;
|
|
411
|
+
width: 100%;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.event-time {
|
|
415
|
+
color: rgba(255, 255, 255, 0.9);
|
|
416
|
+
font-weight: 400;
|
|
417
|
+
font-size: 0.6em;
|
|
418
|
+
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
|
|
419
|
+
white-space: nowrap;
|
|
420
|
+
overflow: hidden;
|
|
421
|
+
text-overflow: ellipsis;
|
|
422
|
+
margin-top: 1px;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.more-events {
|
|
426
|
+
font-size: 0.6em;
|
|
427
|
+
color: #666;
|
|
428
|
+
text-align: center;
|
|
429
|
+
margin-top: 1px;
|
|
430
|
+
font-weight: 500;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
.day:hover {
|
|
434
|
+
background-color: #d0e8ff;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.day.other-month {
|
|
438
|
+
color: #aaa;
|
|
439
|
+
cursor: default;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.today {
|
|
443
|
+
background-color: #ffd54f;
|
|
444
|
+
border: 2px solid #ffa500;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/* Update month view */
|
|
448
|
+
.month-view {
|
|
449
|
+
grid-template-columns: repeat(7, 1fr);
|
|
450
|
+
grid-template-rows: auto repeat(6, 1fr);
|
|
451
|
+
}
|
|
452
|
+
</style>
|