Files
social-graph/frontend/src/components/ContactForm.vue
T
gitrusprusandCursor 986d36ff51 Improve graph/map UX and move contact editing to dedicated pages.
Add viewport controls, collapsible panels with persisted state, filters modal, and refined context menus; preserve map layout on navigation and let contacts be edited with relations on /contacts/:id instead of list modals.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 15:15:45 +03:00

205 lines
6.2 KiB
Vue

<template>
<form @submit.prevent="onSubmit">
<div class="form-group">
<label>Имя *</label>
<input v-model="form.name" class="form-control" placeholder="Фамилия Имя Отчество" required />
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
<div class="form-group">
<label>Email</label>
<input v-model="form.email" type="email" class="form-control" placeholder="email@example.com" />
</div>
<div class="form-group">
<label>Телефон</label>
<input v-model="form.phone" class="form-control" placeholder="+7-900-000-0000" />
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
<div class="form-group">
<label>Организация</label>
<input v-model="form.organization" class="form-control" placeholder="ООО Компания" />
</div>
<div class="form-group">
<label>Должность</label>
<input v-model="form.position" class="form-control" placeholder="Директор" />
</div>
</div>
<div class="form-group">
<label>Карты сети</label>
<div v-if="!availableMaps.length" class="text-muted" style="font-size:13px;">
Нет карт. Создайте карту на странице «Карта сети».
</div>
<div v-else class="map-checkboxes">
<label v-for="map in availableMaps" :key="map.id" class="checkbox-label">
<input
v-model="form.mapIds"
type="checkbox"
:value="String(map.id)"
/>
{{ map.name }}
</label>
</div>
<p class="checkbox-hint">На странице «Граф» контакт виден всегда; на выбранных картах в соответствующих визуализациях.</p>
</div>
<div class="form-group">
<label>Заметки</label>
<textarea v-model="form.notes" class="form-control" placeholder="Дополнительная информация..." rows="3"></textarea>
</div>
<RelationLinkFields
v-if="showRelationLink"
ref="relationLinkRef"
:options="linkToOptions"
:initial-target-id="initialLinkToId"
:conflict-mode="conflictMode"
/>
<component
:is="Ext"
v-for="(Ext, index) in contactFormExtensions"
:key="index"
:contact-id="initial?.id"
v-model="pluginTags"
/>
<div class="contact-form-footer">
<button
v-if="showDelete"
type="button"
class="btn btn-danger"
@click="$emit('delete')"
>
Удалить
</button>
<div class="contact-form-actions">
<button type="button" class="btn btn-secondary" @click="$emit('cancel')">Отмена</button>
<button type="submit" class="btn btn-primary">Сохранить</button>
</div>
</div>
</form>
</template>
<script setup>
import { reactive, ref, watch, computed, onMounted } from 'vue'
import { useNetworkMapsStore } from '../stores/networkMaps'
import { listMembershipsByContact } from '../application/usecases/networkMaps'
import { getContactFormExtensions } from '../core/pluginRegistry'
import RelationLinkFields from './RelationLinkFields.vue'
const props = defineProps({
initial: { type: Object, default: () => ({}) },
initialMapIds: { type: Array, default: null },
deletable: { type: Boolean, default: false },
showRelationLink: { type: Boolean, default: false },
linkToOptions: { type: Array, default: () => [] },
initialLinkToId: { type: [String, Number], default: '' },
conflictMode: { type: Boolean, default: false },
})
const emit = defineEmits(['submit', 'cancel', 'delete'])
const mapsStore = useNetworkMapsStore()
const contactFormExtensions = getContactFormExtensions()
const pluginTags = ref([])
const relationLinkRef = ref(null)
const showDelete = computed(() => {
if (props.deletable) return true
const id = props.initial?.id
return id !== undefined && id !== null && id !== ''
})
const availableMaps = computed(() => mapsStore.maps)
const form = reactive({
name: props.initial.name || '',
email: props.initial.email || '',
phone: props.initial.phone || '',
organization: props.initial.organization || '',
position: props.initial.position || '',
notes: props.initial.notes || '',
mapIds: (props.initialMapIds || []).map(String),
})
async function loadMapIdsForContact(contactId) {
if (!contactId) {
form.mapIds = []
return
}
const memberships = await listMembershipsByContact(contactId)
form.mapIds = memberships.map((m) => String(m.mapId || m.map))
}
watch(() => props.initial, async (v) => {
if (!v || !Object.keys(v).length) {
Object.assign(form, {
name: '',
email: '',
phone: '',
organization: '',
position: '',
notes: '',
mapIds: [],
})
return
}
Object.assign(form, {
name: v.name || '',
email: v.email || '',
phone: v.phone || '',
organization: v.organization || '',
position: v.position || '',
notes: v.notes || '',
})
if (props.initialMapIds) {
form.mapIds = props.initialMapIds.map(String)
} else if (v.id) {
await loadMapIdsForContact(v.id)
}
}, { deep: true })
onMounted(async () => {
await mapsStore.fetchMaps()
if (props.initial?.id && !props.initialMapIds) {
await loadMapIdsForContact(props.initial.id)
}
})
function onSubmit() {
const { mapIds, ...contactData } = form
const relationLink = props.showRelationLink
? relationLinkRef.value?.getRelationLink?.() ?? null
: null
emit('submit', contactData, mapIds, { tags: [...pluginTags.value] }, relationLink)
}
</script>
<style scoped>
.map-checkboxes {
display: flex;
flex-direction: column;
gap: 8px;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
cursor: pointer;
}
.checkbox-hint {
margin: 6px 0 0;
font-size: 12px;
color: var(--text-muted);
line-height: 1.4;
}
.contact-form-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: 22px;
}
.contact-form-actions {
display: flex;
gap: 8px;
margin-left: auto;
}
</style>