This commit is contained in:
2026-06-15 21:04:54 +03:00
parent 5aa7478124
commit a9c58935f2
5 changed files with 2202 additions and 648 deletions
+481
View File
@@ -0,0 +1,481 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<title>Комитеты бизнес-объединений ДНР</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Inter', sans-serif; background: #f8fafc; color: #0f172a; line-height: 1.5; padding: 2rem 1rem; }
.container { max-width: 1400px; margin: 0 auto; }
.header { text-align: center; margin-bottom: 2rem; }
.header h1 { font-size: 2rem; font-weight: 700; background: linear-gradient(135deg, #1e3a5f, #0f2b40); -webkit-background-clip: text; background-clip: text; color: transparent; }
.header p { color: #475569; margin-top: 0.5rem; }
.back-link {
display: inline-block;
margin-bottom: 1.5rem;
color: #1e3a5f;
text-decoration: none;
font-weight: 500;
}
.back-link:hover { text-decoration: underline; }
/* Фильтры */
.filters {
background: white;
border-radius: 20px;
padding: 1.5rem;
margin-bottom: 2rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
border: 1px solid #eef2f6;
}
.filter-group {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 1rem;
}
.filter-group label {
font-weight: 600;
color: #475569;
}
.org-filter {
flex: 1;
min-width: 250px;
padding: 10px 14px;
border-radius: 12px;
border: 1px solid #cbd5e1;
font-family: 'Inter', sans-serif;
font-size: 0.9rem;
background: white;
}
.status-filter {
padding: 10px 14px;
border-radius: 12px;
border: 1px solid #cbd5e1;
font-family: 'Inter', sans-serif;
font-size: 0.9rem;
background: white;
}
.search-input {
flex: 2;
min-width: 250px;
padding: 10px 14px;
border-radius: 12px;
border: 1px solid #cbd5e1;
font-family: 'Inter', sans-serif;
font-size: 0.9rem;
}
.stats {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid #e2e8f0;
font-size: 0.85rem;
color: #64748b;
}
/* Сетка комитетов */
.committees-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
gap: 1.5rem;
}
.committee-card {
background: white;
border-radius: 20px;
border: 1px solid #eef2f6;
overflow: hidden;
transition: all 0.2s;
box-shadow: 0 1px 3px rgba(0,0,0,0.03);
}
.committee-card:hover {
border-color: #cbd5e1;
box-shadow: 0 8px 20px rgba(0,0,0,0.05);
transform: translateY(-2px);
}
.card-header {
padding: 1.2rem;
background: linear-gradient(135deg, #1e3a5f, #0f2b40);
color: white;
}
.org-badge {
display: inline-block;
background: rgba(255,255,255,0.2);
padding: 4px 10px;
border-radius: 20px;
font-size: 0.7rem;
font-weight: 500;
margin-bottom: 0.75rem;
}
.committee-name {
font-size: 1.1rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.committee-status {
display: inline-block;
font-size: 0.7rem;
padding: 3px 10px;
border-radius: 20px;
background: rgba(255,255,255,0.15);
}
.card-body {
padding: 1.2rem;
}
.leader-section {
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid #eef2f6;
}
.leader-title {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #6c86a3;
font-weight: 600;
margin-bottom: 0.5rem;
}
.leader-name {
font-weight: 700;
color: #0f172a;
margin-bottom: 0.25rem;
}
.leader-contacts {
font-size: 0.75rem;
color: #475569;
}
.leader-contacts a {
color: #1e3a5f;
text-decoration: none;
}
.leader-contacts a:hover { text-decoration: underline; }
.leader-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
}
.tag {
background: #e2e8f0;
padding: 2px 8px;
border-radius: 16px;
font-size: 0.7rem;
color: #334155;
}
.committee-description {
font-size: 0.85rem;
color: #475569;
line-height: 1.5;
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid #eef2f6;
}
.expand-btn {
background: none;
border: none;
color: #1e3a5f;
cursor: pointer;
font-size: 0.8rem;
margin-top: 0.5rem;
font-weight: 500;
}
.full-description {
display: none;
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid #e2e8f0;
font-size: 0.85rem;
color: #334155;
line-height: 1.5;
}
.full-description.open { display: block; }
.no-results {
text-align: center;
padding: 3rem;
color: #64748b;
background: white;
border-radius: 20px;
}
.loading {
text-align: center;
padding: 3rem;
color: #64748b;
}
.footer-note {
margin-top: 2rem;
text-align: center;
font-size: 0.75rem;
color: #6c86a3;
border-top: 1px solid #e2edf7;
padding-top: 2rem;
}
@media (max-width: 700px) {
.committees-grid { grid-template-columns: 1fr; }
.filter-group { flex-direction: column; align-items: stretch; }
.org-filter, .status-filter, .search-input { width: 100%; }
}
</style>
</head>
<body>
<div class="container">
<a href="/" class="back-link"><i class="fas fa-arrow-left"></i> Назад к списку организаций</a>
<div class="header">
<h1><i class="fas fa-users"></i> Комитеты бизнес-объединений</h1>
<p>Детальная информация о комитетах, их руководителях и деятельности</p>
</div>
<div class="filters">
<div class="filter-group">
<label><i class="fas fa-building"></i> Организация:</label>
<select id="orgFilter" class="org-filter">
<option value="all">Все организации</option>
</select>
<label><i class="fas fa-flag-checkered"></i> Статус:</label>
<select id="statusFilter" class="status-filter">
<option value="all">Все статусы</option>
<option value="active">Активные</option>
<option value="medium">Средняя активность</option>
<option value="inactive">Неактивные</option>
<option value="candidate">С кандидатом</option>
</select>
<label><i class="fas fa-search"></i> Поиск:</label>
<input type="text" id="searchInput" class="search-input" placeholder="Название комитета, руководитель...">
</div>
<div class="stats" id="stats">
Загрузка...
</div>
</div>
<div id="committeesContainer" class="committees-grid">
<div class="loading"><i class="fas fa-spinner fa-spin"></i> Загрузка комитетов...</div>
</div>
<div class="footer-note">
<i class="fas fa-info-circle"></i> Данные о комитетах загружаются из папки committees/.
Статус активности определяется на основе информации о руководителе.
</div>
</div>
<script>
let allCommittees = [];
let organizationsList = [];
async function loadOrganizations() {
try {
const response = await fetch('/data.json');
if (response.ok) {
const data = await response.json();
organizationsList = data.organizations || [];
// Добавляем также дополнительные организации, если есть комитеты
const additional = data.additionalOrganizations || [];
organizationsList = [...organizationsList, ...additional];
}
} catch (error) {
console.error('Ошибка загрузки организаций:', error);
}
}
async function loadCommittees() {
const committeeFiles = [
{ file: '/committees/opora_committees.json', orgName: 'ОПОРА РОССИИ' },
// Добавьте другие файлы комитетов по мере появления:
// { file: '/committees/deloros_committees.json', orgName: 'Деловая Россия' },
// { file: '/committees/spp_committees.json', orgName: 'Союз промышленников и предпринимателей ДНР' },
];
allCommittees = [];
for (const source of committeeFiles) {
try {
const response = await fetch(source.file);
if (response.ok) {
const committees = await response.json();
committees.forEach(committee => {
allCommittees.push({
...committee,
organization: source.orgName,
organizationId: source.orgName.toLowerCase().replace(/[^a-z0-9]/g, '_')
});
});
}
} catch (error) {
console.log(`Файл ${source.file} не найден`);
}
}
renderFilters();
renderCommittees();
}
function renderFilters() {
const orgSelect = document.getElementById('orgFilter');
const uniqueOrgs = [...new Set(allCommittees.map(c => c.organization))];
orgSelect.innerHTML = '<option value="all">Все организации</option>';
uniqueOrgs.forEach(org => {
const option = document.createElement('option');
option.value = org;
option.textContent = org;
orgSelect.appendChild(option);
});
}
function getLeaderStatus(leader) {
if (!leader || !leader.status) return 'unknown';
return leader.status;
}
function filterCommittees() {
const orgFilter = document.getElementById('orgFilter').value;
const statusFilter = document.getElementById('statusFilter').value;
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
return allCommittees.filter(committee => {
// Фильтр по организации
if (orgFilter !== 'all' && committee.organization !== orgFilter) return false;
// Фильтр по статусу
if (statusFilter !== 'all') {
const leaderStatus = getLeaderStatus(committee.current_leader);
if (statusFilter === 'candidate') {
if (!committee.candidate_for_leader?.name) return false;
} else if (leaderStatus !== statusFilter) return false;
}
// Поиск
if (searchTerm) {
const searchFields = [
committee.name,
committee.current_leader?.name,
committee.current_leader?.title,
committee.candidate_for_leader?.name,
committee.description
].filter(Boolean).join(' ').toLowerCase();
if (!searchFields.includes(searchTerm)) return false;
}
return true;
});
}
function renderCommittees() {
const container = document.getElementById('committeesContainer');
const filtered = filterCommittees();
const stats = document.getElementById('stats');
stats.innerHTML = `Найдено: ${filtered.length} комитетов из ${allCommittees.length}`;
if (filtered.length === 0) {
container.innerHTML = '<div class="no-results"><i class="fas fa-inbox"></i><br>Комитеты не найдены. Попробуйте изменить параметры фильтрации.</div>';
return;
}
container.innerHTML = '';
filtered.forEach((committee, idx) => {
const leader = committee.current_leader;
const candidate = committee.candidate_for_leader;
const statusClass = leader?.status === 'active' ? 'Активен' : (leader?.status === 'medium' ? 'Средняя активность' : 'Статус не определён');
const card = document.createElement('div');
card.className = 'committee-card';
card.innerHTML = `
<div class="card-header">
<div class="org-badge"><i class="fas fa-building"></i> ${escapeHtml(committee.organization)}</div>
<div class="committee-name">${escapeHtml(committee.name)}</div>
<div class="committee-status">${statusClass}</div>
</div>
<div class="card-body">
<div class="leader-section">
<div class="leader-title"><i class="fas fa-user-tie"></i> Текущий руководитель</div>
<div class="leader-name">${leader?.name ? escapeHtml(leader.name) : '—'}</div>
<div class="leader-contacts">
${leader?.contacts?.email ? `<i class="fas fa-envelope"></i> <a href="mailto:${escapeHtml(leader.contacts.email)}">${escapeHtml(leader.contacts.email)}</a><br>` : ''}
${leader?.contacts?.phone ? `<i class="fas fa-phone"></i> <a href="tel:${escapeHtml(leader.contacts.phone)}">${escapeHtml(leader.contacts.phone)}</a><br>` : ''}
${leader?.contacts?.telegram ? `<i class="fab fa-telegram"></i> <a href="https://t.me/${escapeHtml(leader.contacts.telegram.replace('@', ''))}" target="_blank">${escapeHtml(leader.contacts.telegram)}</a><br>` : ''}
</div>
<div class="leader-tags">
${leader?.title ? `<span class="tag">${escapeHtml(leader.title)}</span>` : ''}
${leader?.additional_info ? `<span class="tag">${escapeHtml(leader.additional_info.substring(0, 50))}${leader.additional_info.length > 50 ? '...' : ''}</span>` : ''}
</div>
</div>
${candidate?.name ? `
<div class="leader-section">
<div class="leader-title"><i class="fas fa-user-plus"></i> Кандидат на должность</div>
<div class="leader-name">${escapeHtml(candidate.name)}</div>
<div class="leader-contacts">
${candidate.contacts?.email ? `<i class="fas fa-envelope"></i> <a href="mailto:${escapeHtml(candidate.contacts.email)}">${escapeHtml(candidate.contacts.email)}</a><br>` : ''}
${candidate.contacts?.phone ? `<i class="fas fa-phone"></i> <a href="tel:${escapeHtml(candidate.contacts.phone)}">${escapeHtml(candidate.contacts.phone)}</a><br>` : ''}
${candidate.contacts?.telegram ? `<i class="fab fa-telegram"></i> <a href="https://t.me/${escapeHtml(candidate.contacts.telegram.replace('@', ''))}" target="_blank">${escapeHtml(candidate.contacts.telegram)}</a><br>` : ''}
</div>
<div class="leader-tags">
${candidate.title ? `<span class="tag">${escapeHtml(candidate.title)}</span>` : ''}
${candidate.additional_info ? `<span class="tag">${escapeHtml(candidate.additional_info.substring(0, 50))}${candidate.additional_info.length > 50 ? '...' : ''}</span>` : ''}
</div>
</div>
` : ''}
<div class="committee-description">
<strong>О комитете:</strong><br>
${escapeHtml(committee.description.substring(0, 200))}${committee.description.length > 200 ? '...' : ''}
</div>
${committee.description.length > 200 ? `
<button class="expand-btn" onclick="toggleDescription(this)">
<i class="fas fa-chevron-down"></i> Читать полностью
</button>
<div class="full-description">
${escapeHtml(committee.description)}
</div>
` : ''}
</div>
`;
container.appendChild(card);
});
}
function toggleDescription(btn) {
const fullDesc = btn.nextElementSibling;
const icon = btn.querySelector('i');
if (fullDesc.classList.contains('open')) {
fullDesc.classList.remove('open');
icon.className = 'fas fa-chevron-down';
btn.innerHTML = '<i class="fas fa-chevron-down"></i> Читать полностью';
} else {
fullDesc.classList.add('open');
icon.className = 'fas fa-chevron-up';
btn.innerHTML = '<i class="fas fa-chevron-up"></i> Свернуть';
}
}
function escapeHtml(str) {
if (!str) return '';
return str.replace(/[&<>]/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[m]));
}
// Добавляем обработчики фильтров
document.getElementById('orgFilter').addEventListener('change', () => renderCommittees());
document.getElementById('statusFilter').addEventListener('change', () => renderCommittees());
document.getElementById('searchInput').addEventListener('input', () => renderCommittees());
// Загрузка данных
async function init() {
await loadOrganizations();
await loadCommittees();
}
init();
</script>
</body>
</html>