Add conflictology map mode and fix graph rendering reliability.
Support conflict types, involvement sizing, and map-type toggles while keeping polar sector layout; improve GraphView init retries and edge matching on network maps. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -13,16 +13,22 @@
|
||||
<span class="relation-arrow">→</span>
|
||||
<strong style="color:var(--text)">{{ targetName }}</strong>
|
||||
</p>
|
||||
<p v-if="conflictMode" class="text-muted direction-hint">
|
||||
Направление: кто оказывает давление (агрессор) → на кого (жертва).
|
||||
</p>
|
||||
|
||||
<button class="btn btn-secondary btn-sm swap-btn" type="button" @click="swapEnds">
|
||||
⇄ Поменять направление
|
||||
</button>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Тип связи</label>
|
||||
<RelationTypeSelect v-model="form.type" />
|
||||
<label>{{ conflictMode ? 'Тип связи в конфликте' : 'Тип связи' }}</label>
|
||||
<RelationTypeSelect
|
||||
v-model="form.type"
|
||||
:options="conflictMode ? conflictTypes : undefined"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div v-if="!conflictMode" class="form-group">
|
||||
<label>Интенсивность общения</label>
|
||||
<InteractionIntensitySelect v-model="form.intensity" />
|
||||
</div>
|
||||
@@ -46,11 +52,15 @@ import { computed, ref, watch } from 'vue'
|
||||
import { useContactsStore } from '../stores/contacts'
|
||||
import InteractionIntensitySelect from './InteractionIntensitySelect.vue'
|
||||
import RelationTypeSelect from './RelationTypeSelect.vue'
|
||||
import { CONFLICT_RELATION_TYPES } from '../domain/conflictology'
|
||||
|
||||
const conflictTypes = CONFLICT_RELATION_TYPES
|
||||
|
||||
const props = defineProps({
|
||||
open: { type: Boolean, default: false },
|
||||
source: { type: Object, default: null },
|
||||
target: { type: Object, default: null },
|
||||
conflictMode: { type: Boolean, default: false },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'created'])
|
||||
@@ -79,8 +89,8 @@ watch(
|
||||
swapped.value = false
|
||||
error.value = ''
|
||||
form.value = {
|
||||
type: 'acquaintance',
|
||||
intensity: 'intense',
|
||||
type: props.conflictMode ? 'conflict_neutral' : 'acquaintance',
|
||||
intensity: 'sparse',
|
||||
description: '',
|
||||
}
|
||||
}
|
||||
@@ -128,4 +138,8 @@ async function submit() {
|
||||
.swap-btn {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.direction-hint {
|
||||
margin: -4px 0 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,12 +13,18 @@
|
||||
<span class="relation-arrow">→</span>
|
||||
<strong style="color:var(--text)">{{ targetName }}</strong>
|
||||
</p>
|
||||
<p v-if="conflictMode" class="text-muted direction-hint">
|
||||
Направление: агрессор → жертва.
|
||||
</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Тип связи</label>
|
||||
<RelationTypeSelect v-model="form.type" />
|
||||
<label>{{ conflictMode ? 'Тип связи в конфликте' : 'Тип связи' }}</label>
|
||||
<RelationTypeSelect
|
||||
v-model="form.type"
|
||||
:options="conflictMode ? conflictTypes : undefined"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div v-if="!conflictMode" class="form-group">
|
||||
<label>Интенсивность общения</label>
|
||||
<InteractionIntensitySelect v-model="form.intensity" />
|
||||
</div>
|
||||
@@ -54,10 +60,14 @@ import { computed, ref, watch } from 'vue'
|
||||
import { useContactsStore } from '../stores/contacts'
|
||||
import InteractionIntensitySelect from './InteractionIntensitySelect.vue'
|
||||
import RelationTypeSelect from './RelationTypeSelect.vue'
|
||||
import { CONFLICT_RELATION_TYPES } from '../domain/conflictology'
|
||||
|
||||
const conflictTypes = CONFLICT_RELATION_TYPES
|
||||
|
||||
const props = defineProps({
|
||||
open: { type: Boolean, default: false },
|
||||
relation: { type: Object, default: null },
|
||||
conflictMode: { type: Boolean, default: false },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'updated', 'deleted'])
|
||||
@@ -82,8 +92,8 @@ watch(
|
||||
if (!props.open || !props.relation) return
|
||||
error.value = ''
|
||||
form.value = {
|
||||
type: props.relation.relation_type || 'acquaintance',
|
||||
intensity: props.relation.interaction_intensity || 'intense',
|
||||
type: props.relation.relation_type || (props.conflictMode ? 'conflict_neutral' : 'acquaintance'),
|
||||
intensity: props.relation.interaction_intensity || 'sparse',
|
||||
description: props.relation.description || '',
|
||||
}
|
||||
},
|
||||
@@ -140,6 +150,10 @@ async function onDelete() {
|
||||
margin: 0 8px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.direction-hint {
|
||||
margin: -8px 0 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.relation-edit-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { RELATION_TYPES } from '../domain/networkChoices'
|
||||
import { CONFLICT_RELATION_TYPES } from '../domain/conflictology'
|
||||
|
||||
const props = defineProps({
|
||||
open: { type: Boolean, default: false },
|
||||
@@ -35,7 +36,10 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['close', 'edit'])
|
||||
|
||||
const typeLabels = Object.fromEntries(RELATION_TYPES.map((r) => [r.value, r.label]))
|
||||
const typeLabels = Object.fromEntries([
|
||||
...RELATION_TYPES,
|
||||
...CONFLICT_RELATION_TYPES,
|
||||
].map((r) => [r.value, r.label]))
|
||||
|
||||
const edgeTitle = computed(() => {
|
||||
if (!props.edge) return 'Связь'
|
||||
|
||||
@@ -14,10 +14,22 @@
|
||||
<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 ? ' (по умолчанию)' : '' }}
|
||||
{{ type.name }}{{ type.isDefault ? ' (по умолчанию)' : '' }}{{ type.conflictologyEnabled ? ' · конфликтология' : '' }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="selectedMapType?.conflictologyEnabled" class="form-group">
|
||||
<label>Предмет / инцидент конфликта (в центре карты) *</label>
|
||||
<input
|
||||
v-model="form.conflictSubject"
|
||||
class="form-control"
|
||||
required
|
||||
placeholder="Например: Спор о зоне ответственности"
|
||||
/>
|
||||
<p class="text-muted field-hint">
|
||||
Секторы и круги задаются в типе карты; здесь — только название конфликта в центре.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Описание</label>
|
||||
<textarea
|
||||
@@ -64,8 +76,13 @@ const form = reactive({
|
||||
name: '',
|
||||
description: '',
|
||||
mapTypeId: '',
|
||||
conflictSubject: '',
|
||||
})
|
||||
|
||||
const selectedMapType = computed(() =>
|
||||
props.mapTypes.find((t) => String(t.id) === String(form.mapTypeId)) || null
|
||||
)
|
||||
|
||||
watch(
|
||||
() => [props.open, props.initial, props.defaultMapTypeId, props.mapTypes],
|
||||
() => {
|
||||
@@ -75,15 +92,33 @@ watch(
|
||||
form.mapTypeId = String(
|
||||
props.initial?.mapTypeId || props.defaultMapTypeId || props.mapTypes[0]?.id || ''
|
||||
)
|
||||
form.conflictSubject = props.initial?.conflictSubject
|
||||
|| props.initial?.conflict_subject
|
||||
|| ''
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => [form.mapTypeId, form.name, selectedMapType.value?.conflictologyEnabled],
|
||||
() => {
|
||||
if (!selectedMapType.value?.conflictologyEnabled) {
|
||||
form.conflictSubject = ''
|
||||
return
|
||||
}
|
||||
if (!form.conflictSubject.trim() && form.name.trim()) {
|
||||
form.conflictSubject = form.name.trim()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function onSubmit() {
|
||||
const conflictType = selectedMapType.value?.conflictologyEnabled
|
||||
emit('submit', {
|
||||
name: form.name.trim(),
|
||||
description: form.description.trim(),
|
||||
mapTypeId: form.mapTypeId,
|
||||
conflictSubject: conflictType ? form.conflictSubject.trim() : '',
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -101,4 +136,9 @@ function onSubmit() {
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
}
|
||||
.field-hint {
|
||||
margin: 6px 0 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
</div>
|
||||
<div class="network-map-actions">
|
||||
<slot name="toolbar" />
|
||||
<slot name="filters" />
|
||||
<button class="btn btn-secondary btn-sm" @click="$emit('fit')">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7" />
|
||||
@@ -28,7 +27,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="!collapsed">
|
||||
<div v-show="!collapsed" class="network-map-legend">
|
||||
<slot name="legend" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -50,17 +49,20 @@ defineEmits(['toggle-collapse', 'fit'])
|
||||
.network-map-top-panel {
|
||||
position: relative;
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.network-map-top-panel.collapsed {
|
||||
height: 26px;
|
||||
min-height: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
.panel-toggle-btn {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 8px;
|
||||
z-index: 3;
|
||||
width: 24px;
|
||||
height: 18px;
|
||||
bottom: -11px;
|
||||
left: 50%;
|
||||
z-index: 4;
|
||||
width: 28px;
|
||||
height: 20px;
|
||||
margin-left: -14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: var(--surface-alt);
|
||||
@@ -103,6 +105,9 @@ defineEmits(['toggle-collapse', 'fit'])
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
padding-left: 16px;
|
||||
padding-right: 24px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
.network-map-legend {
|
||||
padding: 0 28px 14px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11,6 +11,17 @@
|
||||
<input v-model="form.name" class="form-control" required placeholder="Например: Корпоративная" />
|
||||
</div>
|
||||
|
||||
<div class="form-group checkbox-row">
|
||||
<label class="checkbox-label">
|
||||
<input v-model="form.conflictologyEnabled" type="checkbox" />
|
||||
Конфликтология
|
||||
</label>
|
||||
<p class="text-muted checkbox-hint">
|
||||
Цветовая кодировка связей, стрелки давления, центр карты — предмет конфликта.
|
||||
Секторы и круги настраиваются ниже как обычно.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Секторы (подписи по кругу)</label>
|
||||
<div v-for="(sector, index) in form.sectors" :key="sector._id" class="dynamic-row">
|
||||
@@ -97,6 +108,7 @@ const form = reactive({
|
||||
name: '',
|
||||
sectors: [],
|
||||
circles: [],
|
||||
conflictologyEnabled: false,
|
||||
})
|
||||
|
||||
function blankSector(label = '') {
|
||||
@@ -119,6 +131,9 @@ function resetForm() {
|
||||
label: c.label,
|
||||
key: c.key,
|
||||
}))
|
||||
form.conflictologyEnabled = Boolean(
|
||||
props.initial?.conflictologyEnabled ?? props.initial?.conflictology_enabled
|
||||
)
|
||||
formError.value = ''
|
||||
}
|
||||
|
||||
@@ -168,6 +183,7 @@ function onSubmit() {
|
||||
name: form.name.trim(),
|
||||
sectors,
|
||||
circles,
|
||||
conflictologyEnabled: form.conflictologyEnabled,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -200,4 +216,19 @@ function onSubmit() {
|
||||
.text-danger {
|
||||
color: var(--red, #e74c3c);
|
||||
}
|
||||
.checkbox-row {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
.checkbox-hint {
|
||||
margin: 6px 0 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user