Add network map UX and theme support.

Introduce network map view/components with filtering and positioning, expand contacts/map backend fields and migrations, and add a persistent light/dark theme toggle with graph label color updates for readability.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-28 13:24:48 +03:00
co-authored by Cursor
parent 81ba6cd076
commit b668ee8507
49 changed files with 4679 additions and 129 deletions
+115 -2
View File
@@ -24,6 +24,35 @@
<input v-model="form.position" class="form-control" placeholder="Директор" />
</div>
</div>
<div class="contact-form-map-fields">
<div class="form-group">
<label>Сфера жизни</label>
<SearchableSelect
v-model="form.life_sphere"
:options="lifeSpheres"
placeholder="Сфера жизни..."
/>
</div>
<div class="form-group">
<label>Круг сети</label>
<SearchableSelect
v-model="form.network_circle"
:options="networkCircles"
placeholder="Круг сети..."
/>
</div>
<div class="form-group">
<label>Важность (15)</label>
<input v-model.number="form.importance" type="number" min="1" max="5" class="form-control" />
</div>
</div>
<div class="form-group checkbox-row">
<label class="checkbox-label">
<input v-model="form.include_on_network_map" type="checkbox" />
Показывать на карте сети
</label>
<p class="checkbox-hint">На странице «Граф» контакт виден всегда; на «Карте сети» только с этой отметкой.</p>
</div>
<div class="form-group">
<label>Заметки</label>
<textarea v-model="form.notes" class="form-control" placeholder="Дополнительная информация..." rows="3"></textarea>
@@ -36,11 +65,30 @@
</template>
<script setup>
import { reactive, watch } from 'vue'
import { reactive, watch, ref, onMounted } from 'vue'
import api from '../api'
import SearchableSelect from './SearchableSelect.vue'
const FALLBACK_SPHERES = [
{ value: 'work', label: 'Работа' },
{ value: 'study', label: 'Учёба' },
{ value: 'hobby', label: 'Хобби' },
{ value: 'family', label: 'Семья' },
{ value: 'health', label: 'Здоровье' },
{ value: 'other', label: 'Другое' },
]
const FALLBACK_CIRCLES = [
{ value: 'support', label: 'Круг поддержки' },
{ value: 'productivity', label: 'Круг продуктивности' },
{ value: 'development', label: 'Круг развития' },
]
const props = defineProps({ initial: { type: Object, default: () => ({}) } })
const emit = defineEmits(['submit', 'cancel'])
const lifeSpheres = ref([...FALLBACK_SPHERES])
const networkCircles = ref([...FALLBACK_CIRCLES])
const form = reactive({
name: props.initial.name || '',
email: props.initial.email || '',
@@ -48,13 +96,78 @@ const form = reactive({
organization: props.initial.organization || '',
position: props.initial.position || '',
notes: props.initial.notes || '',
life_sphere: props.initial.life_sphere || 'other',
network_circle: props.initial.network_circle || 'productivity',
importance: props.initial.importance ?? 3,
include_on_network_map: Boolean(props.initial.include_on_network_map),
})
watch(() => props.initial, (v) => {
if (v) Object.assign(form, v)
if (!v || !Object.keys(v).length) {
Object.assign(form, {
name: '',
email: '',
phone: '',
organization: '',
position: '',
notes: '',
life_sphere: 'other',
network_circle: 'productivity',
importance: 3,
include_on_network_map: false,
})
return
}
Object.assign(form, {
name: v.name || '',
email: v.email || '',
phone: v.phone || '',
organization: v.organization || '',
position: v.position || '',
notes: v.notes || '',
life_sphere: v.life_sphere || 'other',
network_circle: v.network_circle || 'productivity',
importance: v.importance ?? 3,
include_on_network_map: Boolean(v.include_on_network_map),
})
}, { deep: true })
onMounted(async () => {
try {
const { data } = await api.get('/network-map-choices/')
if (data.life_spheres?.length) lifeSpheres.value = data.life_spheres
if (data.network_circles?.length) networkCircles.value = data.network_circles
} catch {
/* оставляем FALLBACK_* */
}
})
function onSubmit() {
emit('submit', { ...form })
}
</script>
<style scoped>
.contact-form-map-fields {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
}
.checkbox-row {
margin-top: 4px;
}
.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;
}
</style>