Files
MapMil/centers/analytics/frontend/src/components/EditObjectModal.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

240 lines
5.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, watch } from "vue";
import { OBJECT_TYPE_LABELS, OBJECT_TYPES, type MapObject, type ObjectType } from "../types/object";
const props = defineProps<{
object: MapObject;
}>();
const emit = defineEmits<{
close: [];
submit: [payload: {
name: string;
description: string;
type: ObjectType;
created_at: string;
}];
}>();
const name = ref("");
const description = ref("");
const type = ref<ObjectType>("marker");
const createdAt = ref("");
const error = ref("");
const submitting = ref(false);
function toLocalDateTimeValue(iso: string): string {
const date = new Date(iso);
const pad = (value: number) => String(value).padStart(2, "0");
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
}
function resetForm() {
name.value = props.object.name;
description.value = props.object.description;
type.value = props.object.type;
createdAt.value = toLocalDateTimeValue(props.object.created_at);
error.value = "";
}
async function handleSubmit() {
if (!name.value.trim()) {
error.value = "Введите название объекта";
return;
}
if (!createdAt.value) {
error.value = "Укажите дату создания";
return;
}
error.value = "";
submitting.value = true;
try {
emit("submit", {
name: name.value.trim(),
description: description.value.trim(),
type: type.value,
created_at: new Date(createdAt.value).toISOString(),
});
} catch (err) {
error.value = err instanceof Error ? err.message : "Не удалось сохранить изменения";
} finally {
submitting.value = false;
}
}
watch(() => props.object, resetForm, { immediate: true });
</script>
<template>
<div class="overlay" @click.self="emit('close')">
<div class="modal" role="dialog" aria-labelledby="edit-title">
<header>
<h2 id="edit-title">Редактировать объект</h2>
<button type="button" class="close" aria-label="Закрыть" @click="emit('close')">
×
</button>
</header>
<form @submit.prevent="handleSubmit">
<p class="coords">
Координаты: {{ object.latitude.toFixed(6) }}, {{ object.longitude.toFixed(6) }}
<span class="hint">(измените через «Переместить» на карте)</span>
</p>
<label>
Название *
<input v-model="name" type="text" placeholder="Название объекта" required />
</label>
<label>
Тип *
<select v-model="type" required>
<option v-for="item in OBJECT_TYPES" :key="item" :value="item">
{{ OBJECT_TYPE_LABELS[item] }}
</option>
</select>
</label>
<label>
Дата создания *
<input v-model="createdAt" type="datetime-local" required />
</label>
<label>
Описание
<textarea
v-model="description"
rows="4"
placeholder="Описание объекта"
/>
</label>
<p v-if="error" class="error">{{ error }}</p>
<footer>
<button type="button" class="secondary" @click="emit('close')">Отмена</button>
<button type="submit" :disabled="submitting">
{{ submitting ? "Сохранение..." : "Сохранить" }}
</button>
</footer>
</form>
</div>
</div>
</template>
<style scoped>
.overlay {
position: fixed;
inset: 0;
z-index: 1100;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.4);
}
.modal {
width: min(480px, calc(100vw - 2rem));
max-height: calc(100vh - 2rem);
overflow-y: auto;
background: #fff;
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
}
header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.25rem;
border-bottom: 1px solid #e8e8e8;
}
header h2 {
margin: 0;
font-size: 1.125rem;
}
.close {
border: none;
background: none;
font-size: 1.5rem;
line-height: 1;
cursor: pointer;
color: #666;
}
form {
padding: 1.25rem;
}
.coords {
margin: 0 0 1rem;
font-size: 0.875rem;
color: #666;
line-height: 1.5;
}
.hint {
display: block;
font-size: 0.75rem;
color: #999;
}
label {
display: block;
margin-bottom: 1rem;
font-size: 0.875rem;
font-weight: 500;
}
input,
select,
textarea {
display: block;
width: 100%;
margin-top: 0.375rem;
padding: 0.5rem 0.75rem;
border: 1px solid #ccc;
border-radius: 4px;
}
textarea {
resize: vertical;
}
.error {
margin: 0 0 1rem;
color: #c62828;
font-size: 0.875rem;
}
footer {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
button {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
background: #2563eb;
color: #fff;
cursor: pointer;
}
button.secondary {
background: #e8e8e8;
color: #333;
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
</style>