|
|
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
|
|
retainDays, captureInterval,
|
|
|
|
|
TIME_OFFSET_MINUTES, TOTAL_MINUTES, getTagColor, getTagName, mainTags, getSubTags,
|
|
|
|
|
logicalMinutesToTime, logicalMinutesFromTime, formatDuration,
|
|
|
|
|
loadTags, loadEvents
|
|
|
|
|
loadTags, loadEvents, toISODate
|
|
|
|
|
} from "./store";
|
|
|
|
|
import { DBEvent, TimelineItem } from "./types";
|
|
|
|
|
|
|
|
|
|
@@ -31,6 +31,7 @@ const isTagManagerOpen = ref(false);
|
|
|
|
|
const isExportModalOpen = ref(false);
|
|
|
|
|
const isEventModalOpen = ref(false);
|
|
|
|
|
const isCalendarOpen = ref(false);
|
|
|
|
|
const isMouseOverTimeline = ref(false); // Track mouse state to prevent race conditions
|
|
|
|
|
|
|
|
|
|
const calendarRef = ref<HTMLElement | null>(null);
|
|
|
|
|
const calendarMonth = ref(new Date());
|
|
|
|
|
@@ -51,6 +52,26 @@ const endTimePickerRef = ref<HTMLElement | null>(null);
|
|
|
|
|
const isStartTimeOpen = ref(false);
|
|
|
|
|
const isEndTimeOpen = ref(false);
|
|
|
|
|
|
|
|
|
|
const openStartTimePicker = async () => {
|
|
|
|
|
isStartTimeOpen.value = !isStartTimeOpen.value;
|
|
|
|
|
isEndTimeOpen.value = false;
|
|
|
|
|
if (isStartTimeOpen.value) {
|
|
|
|
|
await nextTick();
|
|
|
|
|
const activeItems = document.querySelectorAll('.z-120 .bg-\\[\\#007AFF\\].text-white');
|
|
|
|
|
activeItems.forEach(el => el.scrollIntoView({ block: 'center' }));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const openEndTimePicker = async () => {
|
|
|
|
|
isEndTimeOpen.value = !isEndTimeOpen.value;
|
|
|
|
|
isStartTimeOpen.value = false;
|
|
|
|
|
if (isEndTimeOpen.value) {
|
|
|
|
|
await nextTick();
|
|
|
|
|
const activeItems = document.querySelectorAll('.z-120 .bg-\\[\\#007AFF\\].text-white');
|
|
|
|
|
activeItems.forEach(el => el.scrollIntoView({ block: 'center' }));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// --- Logic ---
|
|
|
|
|
const updateCurrentMinute = () => {
|
|
|
|
|
const now = new Date();
|
|
|
|
|
@@ -125,7 +146,13 @@ const applyTheme = (val: string) => {
|
|
|
|
|
|
|
|
|
|
const loadTimeline = async (autoScroll = false) => {
|
|
|
|
|
if (savePath.value) {
|
|
|
|
|
timelineImages.value = await invoke<TimelineItem[]>("get_timeline", { date: currentDate.value, baseDir: savePath.value });
|
|
|
|
|
const rawImages = await invoke<TimelineItem[]>("get_timeline", { date: currentDate.value, baseDir: savePath.value });
|
|
|
|
|
// Pre-calculate logical minutes to prevent high-cost recalculation on every mouse move
|
|
|
|
|
timelineImages.value = rawImages.map(img => ({
|
|
|
|
|
...img,
|
|
|
|
|
logical_minute: timeToLogicalMinutes(img.time, img.isNextDay)
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
if (autoScroll && timelineRef.value) {
|
|
|
|
|
await nextTick();
|
|
|
|
|
const now = new Date();
|
|
|
|
|
@@ -155,20 +182,56 @@ const timeToLogicalMinutes = (timeStr: string, isNextDay = false) => {
|
|
|
|
|
let t = h * 60 + m + (s / 60); if (isNextDay) t += 1440; return t - TIME_OFFSET_MINUTES;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Request Animation Frame lock for mouse move
|
|
|
|
|
let isMouseMovePending = false;
|
|
|
|
|
const handleTimelineMouseMove = (e: MouseEvent) => {
|
|
|
|
|
if (!timelineRef.value) return;
|
|
|
|
|
const rect = timelineRef.value.getBoundingClientRect();
|
|
|
|
|
const min = Math.floor((e.clientY - rect.top + timelineRef.value.scrollTop) / timelineZoom.value);
|
|
|
|
|
if (min >= 0 && min < 1440) {
|
|
|
|
|
hoveredTime.value = logicalMinutesToTime(min);
|
|
|
|
|
if (isDragging.value) dragEndMin.value = min;
|
|
|
|
|
if (viewMode.value === 'dashboard') return;
|
|
|
|
|
const closest = timelineImages.value.reduce((p, c) => {
|
|
|
|
|
const pd = Math.abs(timeToLogicalMinutes(p.time, p.isNextDay) - min);
|
|
|
|
|
const cd = Math.abs(timeToLogicalMinutes(c.time, c.isNextDay) - min);
|
|
|
|
|
return cd < pd ? c : p;
|
|
|
|
|
}, timelineImages.value[0]);
|
|
|
|
|
if (closest) updatePreview(closest);
|
|
|
|
|
isMouseOverTimeline.value = true;
|
|
|
|
|
|
|
|
|
|
if (isMouseMovePending) return;
|
|
|
|
|
isMouseMovePending = true;
|
|
|
|
|
|
|
|
|
|
const clientY = e.clientY; // Capture coordinate outside RAF
|
|
|
|
|
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
isMouseMovePending = false;
|
|
|
|
|
|
|
|
|
|
// Safety check: If mouse already left before this frame executed, DO NOT update preview
|
|
|
|
|
if (!isMouseOverTimeline.value || !timelineRef.value) return;
|
|
|
|
|
|
|
|
|
|
const rect = timelineRef.value.getBoundingClientRect();
|
|
|
|
|
const min = Math.floor((clientY - rect.top + timelineRef.value.scrollTop) / timelineZoom.value);
|
|
|
|
|
|
|
|
|
|
if (min >= 0 && min < 1440) {
|
|
|
|
|
hoveredTime.value = logicalMinutesToTime(min);
|
|
|
|
|
if (isDragging.value) dragEndMin.value = min;
|
|
|
|
|
if (viewMode.value === 'dashboard') return;
|
|
|
|
|
|
|
|
|
|
if (timelineImages.value.length === 0) return;
|
|
|
|
|
|
|
|
|
|
let closest = timelineImages.value[0];
|
|
|
|
|
let minDiff = Math.abs(closest.logical_minute! - min);
|
|
|
|
|
|
|
|
|
|
for (let i = 1; i < timelineImages.value.length; i++) {
|
|
|
|
|
const img = timelineImages.value[i];
|
|
|
|
|
const diff = Math.abs(img.logical_minute! - min);
|
|
|
|
|
if (diff < minDiff) {
|
|
|
|
|
minDiff = diff;
|
|
|
|
|
closest = img;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (closest) updatePreview(closest);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleTimelineMouseLeave = () => {
|
|
|
|
|
isMouseOverTimeline.value = false; // Immediately flag as left
|
|
|
|
|
hoveredTime.value = null;
|
|
|
|
|
if (viewMode.value === 'dashboard') return;
|
|
|
|
|
if (!isDragging.value && lockedImage.value) {
|
|
|
|
|
updatePreview(lockedImage.value);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
@@ -181,11 +244,17 @@ const handleTimelineMouseUp = () => {
|
|
|
|
|
editingEvent.value = { id: 0, date: currentDate.value, start_minute: start, end_minute: end, main_tag_id: mainTags.value[0]?.id || 0, sub_tag_id: null, content: "" };
|
|
|
|
|
isEventModalOpen.value = true;
|
|
|
|
|
} else if (timelineImages.value.length) {
|
|
|
|
|
const closest = timelineImages.value.reduce((p, c) => {
|
|
|
|
|
const pd = Math.abs(timeToLogicalMinutes(p.time, p.isNextDay) - start);
|
|
|
|
|
const cd = Math.abs(timeToLogicalMinutes(c.time, c.isNextDay) - start);
|
|
|
|
|
return cd < pd ? c : p;
|
|
|
|
|
}, timelineImages.value[0]);
|
|
|
|
|
// Optimized linear search on click
|
|
|
|
|
let closest = timelineImages.value[0];
|
|
|
|
|
let minDiff = Math.abs(closest.logical_minute! - start);
|
|
|
|
|
for (let i = 1; i < timelineImages.value.length; i++) {
|
|
|
|
|
const img = timelineImages.value[i];
|
|
|
|
|
const diff = Math.abs(img.logical_minute! - start);
|
|
|
|
|
if (diff < minDiff) {
|
|
|
|
|
minDiff = diff;
|
|
|
|
|
closest = img;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (closest) { lockedImage.value = closest; viewMode.value = 'preview'; updatePreview(closest); }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@@ -203,10 +272,18 @@ const handleTimelineWheel = (e: WheelEvent) => {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const selectCalendarDate = (date: Date) => {
|
|
|
|
|
currentDate.value = date.toLocaleDateString('sv');
|
|
|
|
|
currentDate.value = toISODate(date);
|
|
|
|
|
isCalendarOpen.value = false;
|
|
|
|
|
selectedImage.value = null; lockedImage.value = null; previewSrc.value = "";
|
|
|
|
|
loadTimeline(true); loadEvents();
|
|
|
|
|
selectedImage.value = null;
|
|
|
|
|
lockedImage.value = null;
|
|
|
|
|
previewSrc.value = "";
|
|
|
|
|
hoveredTime.value = null;
|
|
|
|
|
hoveredEventDetails.value = null;
|
|
|
|
|
dayEvents.value = []
|
|
|
|
|
timelineImages.value = [];
|
|
|
|
|
updateCurrentMinute();
|
|
|
|
|
loadTimeline(true);
|
|
|
|
|
loadEvents();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const calendarDays = computed(() => {
|
|
|
|
|
@@ -273,12 +350,12 @@ const togglePause = async () => {
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div ref="timelineRef" class="flex-1 overflow-y-auto no-scrollbar hover:cursor-crosshair relative" @mousedown="handleTimelineMouseDown" @mousemove="handleTimelineMouseMove" @mouseup="handleTimelineMouseUp" @mouseleave="hoveredTime = null" @wheel="handleTimelineWheel">
|
|
|
|
|
<div ref="timelineRef" class="flex-1 overflow-y-auto no-scrollbar hover:cursor-crosshair relative" @mousedown="handleTimelineMouseDown" @mousemove="handleTimelineMouseMove" @mouseup="handleTimelineMouseUp" @mouseleave="handleTimelineMouseLeave" @wheel="handleTimelineWheel">
|
|
|
|
|
<div :style="{ height: TOTAL_MINUTES * timelineZoom + 'px' }" class="relative ml-14 mr-4">
|
|
|
|
|
<div v-for="h in 24" :key="h" class="absolute left-0 w-full border-t border-border-main/60" :style="{ top: (h-1) * 60 * timelineZoom + 'px' }">
|
|
|
|
|
<span v-if="h > 1" class="absolute -left-11 -top-2.5 text-[10px] font-bold text-text-sec">{{ String((h - 1 + 3) % 24).padStart(2, '0') }}:00</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-for="ev in dayEvents" :key="ev.id" class="absolute left-0 w-[45%] opacity-80 border-l-4 cursor-pointer hover:z-50 hover:brightness-110" :style="{ top: ev.start_minute * timelineZoom + 'px', height: (ev.end_minute - ev.start_minute) * timelineZoom + 'px', backgroundColor: getTagColor(ev.main_tag_id) + '50', borderColor: getTagColor(ev.main_tag_id) }" @click.stop="editingEvent = { ...ev }; isEventModalOpen = true" @mouseenter="hoveredEventDetails = { event: ev, x: $event.clientX, y: $event.clientY }" @mousemove="hoveredEventDetails ? (hoveredEventDetails.x = $event.clientX, hoveredEventDetails.y = $event.clientY) : null" @mouseleave="hoveredEventDetails = null"></div>
|
|
|
|
|
<div v-for="ev in dayEvents" :key="ev.id" class="absolute left-0 w-[45%] opacity-80 border-l-4 cursor-pointer hover:opacity-100 hover:border-l-[6px] hover:z-50 hover:brightness-110 transition-all duration-100" :style="{ top: ev.start_minute * timelineZoom + 'px', height: (ev.end_minute - ev.start_minute) * timelineZoom + 'px', backgroundColor: getTagColor(ev.main_tag_id) + '50', borderColor: getTagColor(ev.main_tag_id) }" @click.stop="editingEvent = { ...ev }; isEventModalOpen = true" @mousedown.stop @mouseenter="hoveredEventDetails = { event: ev, x: $event.clientX, y: $event.clientY }" @mousemove="hoveredEventDetails ? (hoveredEventDetails.x = $event.clientX, hoveredEventDetails.y = $event.clientY) : null" @mouseleave="hoveredEventDetails = null"></div>
|
|
|
|
|
<div v-for="img in timelineImages" :key="img.path" class="absolute left-[50%] right-2 h-0.5 bg-[#007AFF]/20 rounded-full" :class="[selectedImage?.path === img.path ? 'bg-[#007AFF]/60 h-1 z-10' : '', lockedImage?.path === img.path ? 'bg-[#007AFF] h-1.5 ring-2 ring-[#007AFF]/20 z-20' : '']" :style="{ top: timeToLogicalMinutes(img.time, img.isNextDay) * timelineZoom + 'px' }"></div>
|
|
|
|
|
<div v-if="isDragging && dragStartMin !== null && dragEndMin !== null" class="absolute left-0 w-full bg-[#007AFF]/10 border-y-2 border-[#007AFF] pointer-events-none z-30" :style="{ top: Math.min(dragStartMin, dragEndMin) * timelineZoom + 'px', height: Math.abs(dragEndMin - dragStartMin) * timelineZoom + 'px' }"></div>
|
|
|
|
|
<div v-if="hoveredTime" class="absolute left-0 right-0 border-t-2 border-[#007AFF] z-40 pointer-events-none" :style="{ top: timeToLogicalMinutes(hoveredTime, hoveredTime < '03:00') * timelineZoom + 'px' }"><div class="absolute -left-12 -top-3 bg-[#007AFF] text-white text-[9px] px-1 py-0.5 rounded font-bold">{{ hoveredTime }}</div></div>
|
|
|
|
|
@@ -331,20 +408,20 @@ const togglePause = async () => {
|
|
|
|
|
<div class="flex gap-3 items-end">
|
|
|
|
|
<div ref="startTimePickerRef" class="flex-1 relative">
|
|
|
|
|
<label class="text-[10px] font-bold text-text-sec block mb-1">开始时间</label>
|
|
|
|
|
<button @click="isStartTimeOpen = !isStartTimeOpen; isEndTimeOpen = false" class="w-full h-12 bg-bg-input rounded-xl px-4 flex items-center justify-center gap-2 hover:bg-bg-hover transition-all border border-transparent focus:border-[#007AFF]/30"><span class="text-sm font-bold leading-none">{{ logicalMinutesToTime(editingEvent.start_minute) }}</span></button>
|
|
|
|
|
<button @click="openStartTimePicker" class="w-full h-12 bg-bg-input rounded-xl px-4 flex items-center justify-center gap-2 hover:bg-bg-hover transition-all border border-transparent focus:border-[#007AFF]/30"><span class="text-sm font-bold leading-none">{{ logicalMinutesToTime(editingEvent.start_minute) }}</span></button>
|
|
|
|
|
<div v-if="isStartTimeOpen" class="absolute top-full left-0 right-0 mt-2 bg-bg-card rounded-3xl shadow-2xl border border-border-main z-120 p-4 animate-in fade-in slide-in-from-top-2 flex gap-4 h-64">
|
|
|
|
|
<div class="flex-1 overflow-y-auto no-scrollbar flex flex-col gap-1">
|
|
|
|
|
<button v-for="h in 24" :key="h" @click="editingEvent.start_minute = logicalMinutesFromTime(`${String((h-1+3)%24).padStart(2,'0')}:${String(editingEvent.start_minute % 60).padStart(2,'0')}`)" class="py-2 text-[11px] rounded-lg transition-colors w-full text-center" :class="Math.floor((editingEvent.start_minute + TIME_OFFSET_MINUTES) / 60) % 24 === (h-1+3)%24 ? 'bg-[#007AFF] text-white font-bold' : 'hover:bg-bg-input text-main'">{{ String((h-1+3)%24).padStart(2,'0') }}</button>
|
|
|
|
|
<button v-for="h in 24" :key="h" @click="editingEvent.start_minute = logicalMinutesFromTime(`${String((h-1+3)%24).padStart(2,'0')}:${String(editingEvent.start_minute % 60).padStart(2,'0')}`)" class="py-2 text-xs rounded-lg transition-colors w-full text-center" :class="Math.floor((editingEvent.start_minute + TIME_OFFSET_MINUTES) / 60) % 24 === (h-1+3)%24 ? 'bg-[#007AFF] text-white font-bold' : 'hover:bg-bg-input text-main'">{{ String((h-1+3)%24).padStart(2,'0') }}</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex-1 overflow-y-auto no-scrollbar flex flex-col gap-1">
|
|
|
|
|
<button v-for="m in 60" :key="m" @click="editingEvent.start_minute = Math.floor(editingEvent.start_minute / 60) * 60 + (m-1); isStartTimeOpen = false" class="py-2 text-[11px] rounded-lg transition-colors w-full text-center" :class="editingEvent.start_minute % 60 === (m-1) ? 'bg-[#007AFF] text-white font-bold' : 'hover:bg-bg-input text-main'">{{ String(m-1).padStart(2,'0') }}</button>
|
|
|
|
|
<button v-for="m in 60" :key="m" @click="editingEvent.start_minute = Math.floor(editingEvent.start_minute / 60) * 60 + (m-1); isStartTimeOpen = false" class="py-2 text-xs rounded-lg transition-colors w-full text-center" :class="editingEvent.start_minute % 60 === (m-1) ? 'bg-[#007AFF] text-white font-bold' : 'hover:bg-bg-input text-main'">{{ String(m-1).padStart(2,'0') }}</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="h-12 flex items-center justify-center"><span class="text-[11px] font-bold text-text-sec bg-bg-input px-3 py-1.5 rounded-xl border border-border-main/50">{{ formatDuration(editingEvent.start_minute, editingEvent.end_minute) }}</span></div>
|
|
|
|
|
<div ref="endTimePickerRef" class="flex-1 relative">
|
|
|
|
|
<label class="text-[10px] font-bold text-text-sec block mb-1">结束时间</label>
|
|
|
|
|
<button @click="isEndTimeOpen = !isEndTimeOpen; isStartTimeOpen = false" class="w-full h-12 bg-bg-input rounded-xl px-4 flex items-center justify-center gap-2 hover:bg-bg-hover transition-all border border-transparent focus:border-[#007AFF]/30"><span class="text-sm font-bold leading-none">{{ logicalMinutesToTime(editingEvent.end_minute) }}</span></button>
|
|
|
|
|
<button @click="openEndTimePicker" class="w-full h-12 bg-bg-input rounded-xl px-4 flex items-center justify-center gap-2 hover:bg-bg-hover transition-all border border-transparent focus:border-[#007AFF]/30"><span class="text-sm font-bold leading-none">{{ logicalMinutesToTime(editingEvent.end_minute) }}</span></button>
|
|
|
|
|
<div v-if="isEndTimeOpen" class="absolute top-full left-0 right-0 mt-2 bg-bg-card rounded-3xl shadow-2xl border border-border-main z-120 p-4 animate-in fade-in slide-in-from-top-2 flex gap-4 h-64">
|
|
|
|
|
<div class="flex-1 overflow-y-auto no-scrollbar flex flex-col gap-1 text-center">
|
|
|
|
|
<button v-for="h in 24" :key="h" @click="editingEvent.end_minute = logicalMinutesFromTime(`${String((h-1+3)%24).padStart(2,'0')}:${String(editingEvent.end_minute % 60).padStart(2,'0')}`)" class="py-2 text-xs rounded-lg transition-colors w-full text-center" :class="Math.floor((editingEvent.end_minute + TIME_OFFSET_MINUTES) / 60) % 24 === (h-1+3)%24 ? 'bg-[#007AFF] text-white font-bold' : 'hover:bg-bg-input text-main'">{{ String((h-1+3)%24).padStart(2,'0') }}</button>
|
|
|
|
|
@@ -362,7 +439,7 @@ const togglePause = async () => {
|
|
|
|
|
<div v-if="editingEvent.main_tag_id && getSubTags(editingEvent.main_tag_id).length" class="space-y-2"><label class="text-[10px] font-bold text-text-sec">副标签</label>
|
|
|
|
|
<div class="flex flex-wrap gap-2"><button v-for="sub in getSubTags(editingEvent.main_tag_id)" :key="sub.id" @click="editingEvent.sub_tag_id = sub.id" class="px-3 py-1.5 rounded-lg text-[12px] font-medium" :class="editingEvent.sub_tag_id === sub.id ? 'bg-[#1D1D1F] text-white' : 'bg-bg-input text-text-sec'">{{ sub.name }}</button></div>
|
|
|
|
|
</div>
|
|
|
|
|
<textarea v-model="editingEvent.content" placeholder="记录具体内容..." class="w-full bg-bg-input rounded-2xl p-4 text-sm min-h-25 outline-none border border-transparent focus:bg-bg-card focus:border-border-main transition-all"></textarea>
|
|
|
|
|
<textarea v-model="editingEvent.content" placeholder="记录具体内容..." @keydown.ctrl.enter="saveEvent" class="w-full bg-bg-input rounded-2xl p-4 text-sm min-h-25 outline-none border border-transparent focus:bg-bg-card focus:border-border-main transition-all"></textarea>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex gap-4 pt-4">
|
|
|
|
|
<button v-if="editingEvent.id" @click="deleteEvent(editingEvent.id); isEventModalOpen = false" class="text-[#FF3B30] font-bold px-4 py-2 flex items-center gap-2"><Trash2 :size="18" /> 删除</button>
|
|
|
|
|
@@ -374,8 +451,12 @@ const togglePause = async () => {
|
|
|
|
|
|
|
|
|
|
<!-- Fullscreen & Tooltip & Toast -->
|
|
|
|
|
<div v-if="isFullscreen && previewSrc" class="fixed inset-0 z-200 bg-black/95 flex items-center justify-center p-6 backdrop-blur-xl"><button @click="isFullscreen = false" class="absolute top-10 right-10 w-12 h-12 bg-white/10 rounded-full flex items-center justify-center"><X :size="32" class="text-white" /></button><img :src="previewSrc" class="max-w-full max-h-full object-contain shadow-2xl" /></div>
|
|
|
|
|
<div v-if="hoveredEventDetails" class="fixed z-300 pointer-events-none bg-bg-card/90 backdrop-blur-xl border border-white/20 shadow-2xl rounded-2xl p-4 w-64" :style="{ left: hoveredEventDetails.x + 15 + 'px', top: hoveredEventDetails.y + 15 + 'px' }">
|
|
|
|
|
<div class="flex items-center gap-2 mb-2"><div class="w-3 h-3 rounded-full" :style="{ backgroundColor: getTagColor(hoveredEventDetails.event.main_tag_id) }"></div><span class="font-bold text-sm text-text-main">{{ getTagName(hoveredEventDetails.event.main_tag_id) }}</span></div>
|
|
|
|
|
<div v-if="hoveredEventDetails && hoveredEventDetails.event" class="fixed z-300 pointer-events-none bg-bg-card/90 backdrop-blur-xl border border-white/20 shadow-2xl rounded-2xl p-4 w-64" :style="{ left: hoveredEventDetails.x + 15 + 'px', top: hoveredEventDetails.y + 15 + 'px' }">
|
|
|
|
|
<div class="flex items-center gap-2 mb-2">
|
|
|
|
|
<div class="w-3 h-3 rounded-full" :style="{ backgroundColor: getTagColor(hoveredEventDetails.event.main_tag_id) }"></div>
|
|
|
|
|
<span class="font-bold text-sm text-text-main">{{ getTagName(hoveredEventDetails.event.main_tag_id) }}</span>
|
|
|
|
|
<span v-if="hoveredEventDetails.event.sub_tag_id" class="text-[10px] font-bold text-text-sec bg-bg-input px-2 py-0.5 rounded-md border border-border-main/50">{{ getTagName(hoveredEventDetails.event.sub_tag_id) }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex items-center justify-between text-[11px] font-bold text-text-sec mb-2.5"><span>{{ logicalMinutesToTime(hoveredEventDetails.event.start_minute) }} - {{ logicalMinutesToTime(hoveredEventDetails.event.end_minute) }}</span><span class="bg-bg-input px-1.5 py-0.5 rounded-lg text-[#007AFF]">{{ formatDuration(hoveredEventDetails.event.start_minute, hoveredEventDetails.event.end_minute) }}</span></div>
|
|
|
|
|
<div v-if="hoveredEventDetails.event.content" class="text-xs text-text-main leading-relaxed wrap-break-words whitespace-pre-wrap">{{ hoveredEventDetails.event.content }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
@@ -392,4 +473,4 @@ const togglePause = async () => {
|
|
|
|
|
<style>
|
|
|
|
|
.no-scrollbar::-webkit-scrollbar { display: none; }
|
|
|
|
|
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
|
|
|
|
</style>
|
|
|
|
|
</style>
|