Add settings page and configurable network map types.
Let users define sector/circle labels and geometry per map type, choose a type when creating maps, and sync types through local backup and API migrations. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -10,6 +10,14 @@
|
||||
<label>Название *</label>
|
||||
<input v-model="form.name" class="form-control" required placeholder="Например: Коллектив А" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Тип карты *</label>
|
||||
<select v-model="form.mapTypeId" class="form-control" required>
|
||||
<option v-for="type in mapTypes" :key="type.id" :value="String(type.id)">
|
||||
{{ type.name }}{{ type.isDefault ? ' (по умолчанию)' : '' }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Описание</label>
|
||||
<textarea
|
||||
@@ -45,6 +53,8 @@ const props = defineProps({
|
||||
open: { type: Boolean, default: false },
|
||||
initial: { type: Object, default: () => ({}) },
|
||||
deletable: { type: Boolean, default: false },
|
||||
mapTypes: { type: Array, default: () => [] },
|
||||
defaultMapTypeId: { type: String, default: '' },
|
||||
})
|
||||
const emit = defineEmits(['close', 'submit', 'delete'])
|
||||
|
||||
@@ -53,20 +63,28 @@ const isEdit = computed(() => Boolean(props.initial?.id))
|
||||
const form = reactive({
|
||||
name: '',
|
||||
description: '',
|
||||
mapTypeId: '',
|
||||
})
|
||||
|
||||
watch(
|
||||
() => [props.open, props.initial],
|
||||
() => [props.open, props.initial, props.defaultMapTypeId, props.mapTypes],
|
||||
() => {
|
||||
if (!props.open) return
|
||||
form.name = props.initial?.name || ''
|
||||
form.description = props.initial?.description || ''
|
||||
form.mapTypeId = String(
|
||||
props.initial?.mapTypeId || props.defaultMapTypeId || props.mapTypes[0]?.id || ''
|
||||
)
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
function onSubmit() {
|
||||
emit('submit', { name: form.name.trim(), description: form.description.trim() })
|
||||
emit('submit', {
|
||||
name: form.name.trim(),
|
||||
description: form.description.trim(),
|
||||
mapTypeId: form.mapTypeId,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
<template>
|
||||
<div v-if="open" class="modal-overlay" @click.self="$emit('close')">
|
||||
<div class="modal modal-wide">
|
||||
<div class="modal-header">
|
||||
<h3>{{ isEdit ? 'Редактировать тип карты' : 'Новый тип карты' }}</h3>
|
||||
<button class="btn btn-secondary btn-sm" type="button" @click="$emit('close')">✕</button>
|
||||
</div>
|
||||
<form @submit.prevent="onSubmit">
|
||||
<div class="form-group">
|
||||
<label>Название типа *</label>
|
||||
<input v-model="form.name" class="form-control" required placeholder="Например: Корпоративная" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Секторы (подписи по кругу)</label>
|
||||
<div v-for="(sector, index) in form.sectors" :key="sector._id" class="dynamic-row">
|
||||
<input
|
||||
v-model="sector.label"
|
||||
class="form-control"
|
||||
required
|
||||
:placeholder="`Сектор ${index + 1}`"
|
||||
/>
|
||||
<button
|
||||
v-if="form.sectors.length > 1"
|
||||
type="button"
|
||||
class="btn btn-secondary btn-sm"
|
||||
@click="removeSector(index)"
|
||||
>
|
||||
Удалить
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="btn btn-secondary btn-sm" @click="addSector">+ Добавить сектор</button>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Концентрические круги (от центра к краю)</label>
|
||||
<div v-for="(circle, index) in form.circles" :key="circle._id" class="dynamic-row">
|
||||
<input
|
||||
v-model="circle.label"
|
||||
class="form-control"
|
||||
required
|
||||
:placeholder="`Круг ${index + 1}`"
|
||||
/>
|
||||
<button
|
||||
v-if="form.circles.length > 1"
|
||||
type="button"
|
||||
class="btn btn-secondary btn-sm"
|
||||
@click="removeCircle(index)"
|
||||
>
|
||||
Удалить
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="btn btn-secondary btn-sm" @click="addCircle">+ Добавить круг</button>
|
||||
</div>
|
||||
|
||||
<p v-if="formError" class="text-danger" style="font-size:13px;">{{ formError }}</p>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
v-if="isEdit && !initial?.isDefault"
|
||||
type="button"
|
||||
class="btn btn-danger"
|
||||
@click="$emit('delete')"
|
||||
>
|
||||
Удалить
|
||||
</button>
|
||||
<div class="modal-footer-actions">
|
||||
<button type="button" class="btn btn-secondary" @click="$emit('close')">Отмена</button>
|
||||
<button type="submit" class="btn btn-primary">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, watch, computed } from 'vue'
|
||||
import { assignKeysToItems, validateMapTypePayload } from '../domain/mapTypeDefaults'
|
||||
|
||||
let rowId = 0
|
||||
function nextRowId() {
|
||||
rowId += 1
|
||||
return rowId
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
open: { type: Boolean, default: false },
|
||||
initial: { type: Object, default: () => ({}) },
|
||||
})
|
||||
const emit = defineEmits(['close', 'submit', 'delete'])
|
||||
|
||||
const isEdit = computed(() => Boolean(props.initial?.id))
|
||||
const formError = ref('')
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
sectors: [],
|
||||
circles: [],
|
||||
})
|
||||
|
||||
function blankSector(label = '') {
|
||||
return { _id: nextRowId(), label, key: '' }
|
||||
}
|
||||
|
||||
function blankCircle(label = '') {
|
||||
return { _id: nextRowId(), label, key: '' }
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
form.name = props.initial?.name || ''
|
||||
form.sectors = (props.initial?.sectors || [{ label: 'Сектор 1', key: 'sector_1' }]).map((s) => ({
|
||||
_id: nextRowId(),
|
||||
label: s.label,
|
||||
key: s.key,
|
||||
}))
|
||||
form.circles = (props.initial?.circles || [{ label: 'Круг 1', key: 'circle_1' }]).map((c) => ({
|
||||
_id: nextRowId(),
|
||||
label: c.label,
|
||||
key: c.key,
|
||||
}))
|
||||
formError.value = ''
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.open, props.initial],
|
||||
() => {
|
||||
if (!props.open) return
|
||||
resetForm()
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
function addSector() {
|
||||
form.sectors.push(blankSector())
|
||||
}
|
||||
|
||||
function removeSector(index) {
|
||||
form.sectors.splice(index, 1)
|
||||
}
|
||||
|
||||
function addCircle() {
|
||||
form.circles.push(blankCircle())
|
||||
}
|
||||
|
||||
function removeCircle(index) {
|
||||
form.circles.splice(index, 1)
|
||||
}
|
||||
|
||||
function onSubmit() {
|
||||
const sectors = assignKeysToItems(
|
||||
form.sectors.map((s) => ({ label: s.label.trim(), key: s.key || undefined }))
|
||||
)
|
||||
const circles = assignKeysToItems(
|
||||
form.circles.map((c) => ({ label: c.label.trim(), key: c.key || undefined }))
|
||||
)
|
||||
const errors = validateMapTypePayload({
|
||||
name: form.name.trim(),
|
||||
sectors,
|
||||
circles,
|
||||
})
|
||||
if (Object.keys(errors).length) {
|
||||
formError.value = Object.values(errors)[0]
|
||||
return
|
||||
}
|
||||
formError.value = ''
|
||||
emit('submit', {
|
||||
name: form.name.trim(),
|
||||
sectors,
|
||||
circles,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-wide {
|
||||
max-width: 520px;
|
||||
}
|
||||
.dynamic-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.dynamic-row .form-control {
|
||||
flex: 1;
|
||||
}
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.modal-footer-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
}
|
||||
.text-danger {
|
||||
color: var(--red, #e74c3c);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user