Files
MapMil/frontend/src/components/TimelineBar.vue
T
gitrusprusandCursor 9dfe713668 Add CP→CA→PI platform foundation on MapMil monorepo.
Unify parsing workers, analytics API with PostgreSQL, map UI, and PI distribution into centers/ with Docker Compose.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 11:43:27 +03:00

200 lines
3.7 KiB
Vue

<script setup lang="ts">
import { computed } from "vue";
import type { MapObject } from "../types/object";
const props = defineProps<{
modelValue: number;
min: number;
max: number;
objects: MapObject[];
visibleCount: number;
}>();
const emit = defineEmits<{
"update:modelValue": [value: number];
}>();
const range = computed({
get: () => props.modelValue,
set: (value) => emit("update:modelValue", value),
});
const formattedTime = computed(() =>
new Date(props.modelValue).toLocaleString("ru-RU", {
day: "2-digit",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
}),
);
const formattedMin = computed(() =>
new Date(props.min).toLocaleDateString("ru-RU", {
day: "2-digit",
month: "short",
year: "numeric",
}),
);
const formattedMax = computed(() =>
new Date(props.max).toLocaleDateString("ru-RU", {
day: "2-digit",
month: "short",
year: "numeric",
}),
);
const markers = computed(() => {
if (props.max <= props.min) return [];
const span = props.max - props.min;
return props.objects.map((obj) => {
const time = new Date(obj.created_at).getTime();
const percent = ((time - props.min) / span) * 100;
return {
id: obj.id,
name: obj.name,
percent: Math.min(100, Math.max(0, percent)),
visible: time <= props.modelValue,
};
});
});
const hasRange = computed(() => props.max > props.min);
</script>
<template>
<footer class="timeline">
<div class="timeline-header">
<span class="label">Таймлайн появления</span>
<span class="current-time">{{ formattedTime }}</span>
<span class="count">{{ visibleCount }} / {{ objects.length }} объектов</span>
</div>
<div class="slider-wrap">
<span class="edge-label">{{ formattedMin }}</span>
<div class="slider-track">
<div
v-for="marker in markers"
:key="marker.id"
class="object-marker"
:class="{ visible: marker.visible }"
:style="{ left: `${marker.percent}%` }"
:title="marker.name"
/>
<input
v-model.number="range"
class="slider"
type="range"
:min="min"
:max="max"
:step="hasRange ? Math.max(1, Math.floor((max - min) / 500)) : 1"
:disabled="!hasRange"
/>
</div>
<span class="edge-label">{{ formattedMax }}</span>
</div>
</footer>
</template>
<style scoped>
.timeline {
position: relative;
z-index: 3;
flex-shrink: 0;
padding: 0.75rem 1.25rem 1rem;
background: #fff;
border-top: 1px solid #e0e0e0;
}
.timeline-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 0.625rem;
font-size: 0.8125rem;
}
.label {
font-weight: 600;
color: #333;
}
.current-time {
color: #2563eb;
font-weight: 500;
}
.count {
margin-left: auto;
color: #666;
}
.slider-wrap {
display: flex;
align-items: center;
gap: 0.75rem;
}
.edge-label {
flex-shrink: 0;
width: 5.5rem;
font-size: 0.75rem;
color: #888;
text-align: center;
}
.edge-label:first-child {
text-align: left;
}
.edge-label:last-child {
text-align: right;
}
.slider-track {
position: relative;
flex: 1;
height: 2rem;
display: flex;
align-items: center;
}
.object-marker {
position: absolute;
top: 50%;
width: 8px;
height: 8px;
margin-left: -4px;
margin-top: -4px;
border-radius: 50%;
background: #bbb;
pointer-events: none;
z-index: 1;
transition: background 0.15s, transform 0.15s;
}
.object-marker.visible {
background: #2563eb;
transform: scale(1.25);
}
.slider {
position: relative;
z-index: 2;
width: 100%;
margin: 0;
cursor: pointer;
accent-color: #2563eb;
}
.slider:disabled {
cursor: default;
opacity: 0.5;
}
</style>