433 lines
19 KiB
HTML
433 lines
19 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru" data-theme="light">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>Экран сравнения версий</title>
|
||
<link rel="stylesheet" href="design-system/kt-ai-fonts.css">
|
||
<link rel="stylesheet" href="design-system/kt-ai-tokens.css">
|
||
<link rel="stylesheet" href="design-system/kt-ai-components.css">
|
||
<link rel="stylesheet" href="design-system/kt-ai-page.css">
|
||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🔍</text></svg>">
|
||
<style>
|
||
a.kt-ai-btn { text-decoration: none; }
|
||
.comparison-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--kt-ai-space-6); }
|
||
.comparison-panel { display: flex; flex-direction: column; gap: var(--kt-ai-space-4); }
|
||
.screenshot-placeholder {
|
||
background: var(--kt-ai-bg-soft);
|
||
border: 2px dashed var(--kt-ai-border);
|
||
border-radius: var(--kt-ai-radius-3xl);
|
||
aspect-ratio: 16/9;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: var(--kt-ai-fg-muted);
|
||
font-size: var(--kt-ai-text-sm);
|
||
}
|
||
.change-item {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: var(--kt-ai-space-3);
|
||
padding: var(--kt-ai-space-3);
|
||
border-radius: var(--kt-ai-radius-xl);
|
||
background: var(--kt-ai-card-bg);
|
||
border: 1px solid var(--kt-ai-card-border);
|
||
}
|
||
.change-item.accepted { border-color: var(--kt-ai-status-ok-fg); background: var(--kt-ai-status-ok-bg); }
|
||
.change-item.rejected { border-color: var(--kt-ai-status-risk-fg); background: var(--kt-ai-status-risk-bg); }
|
||
.change-icon {
|
||
width: 24px; height: 24px;
|
||
border-radius: var(--kt-ai-radius-md);
|
||
display: flex; align-items: center; justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
.change-icon.add { background: var(--kt-ai-status-ok-fg); color: white; }
|
||
.change-icon.remove { background: var(--kt-ai-status-risk-fg); color: white; }
|
||
.change-icon.modify { background: var(--kt-ai-chart-blue); color: white; }
|
||
.change-content { flex: 1; min-width: 0; }
|
||
.change-title { font-weight: var(--kt-ai-weight-medium); color: var(--kt-ai-fg); margin-bottom: 2px; }
|
||
.change-desc { font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); }
|
||
.filter-bar { display: flex; gap: var(--kt-ai-space-3); align-items: center; flex-wrap: wrap; }
|
||
.stats-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--kt-ai-space-4); }
|
||
@media (max-width: 900px) {
|
||
.comparison-grid { grid-template-columns: 1fr; }
|
||
.stats-row { grid-template-columns: 1fr 1fr; }
|
||
}
|
||
.comment-section { margin-top: var(--kt-ai-space-6); }
|
||
.comment-list { display: flex; flex-direction: column; gap: var(--kt-ai-space-3); margin-top: var(--kt-ai-space-4); }
|
||
.comment-item { padding: var(--kt-ai-space-3); background: var(--kt-ai-bg-soft); border-radius: var(--kt-ai-radius-xl); }
|
||
.comment-meta { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-faint); margin-bottom: var(--kt-ai-space-2); }
|
||
.empty-state { text-align: center; padding: var(--kt-ai-space-12); color: var(--kt-ai-fg-muted); }
|
||
</style>
|
||
</head>
|
||
<body class="kt-ai-app" data-kt-data-mode="demo">
|
||
|
||
<header class="kt-ai-pagehead">
|
||
<div class="kt-ai-wrap kt-ai-pagehead-in">
|
||
<div style="display:flex;align-items:center;gap:var(--kt-ai-space-3)">
|
||
<img src="https://code.vibe42.kz/tore/aitu/kt-logo.svg" alt="Казахтелеком" height="28">
|
||
<strong>Экран сравнения версий</strong>
|
||
</div>
|
||
<span class="kt-ai-spacer"></span>
|
||
<span class="kt-ai-chip" data-tone="orange">Демо-данные</span>
|
||
<button class="kt-ai-btn" data-variant="ghost" id="themeToggle">🌓 Тема</button>
|
||
<button class="kt-ai-btn" data-variant="primary" id="approveAllBtn">Принять все</button>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="kt-ai-wrap" style="padding: var(--kt-ai-space-8) 0">
|
||
|
||
<!-- KPI полоса -->
|
||
<div class="kt-ai-kpi-strip" style="margin-bottom: var(--kt-ai-space-8)">
|
||
<div class="kt-ai-kpi">
|
||
<div class="value" id="kpi-total">0</div>
|
||
<div class="label">Всего правок</div>
|
||
</div>
|
||
<div class="kt-ai-kpi">
|
||
<div class="value" id="kpi-accepted" style="color: var(--kt-ai-status-ok-fg)">0</div>
|
||
<div class="label">Принято</div>
|
||
</div>
|
||
<div class="kt-ai-kpi">
|
||
<div class="value" id="kpi-pending" style="color: var(--kt-ai-chart-orange)">0</div>
|
||
<div class="label">На проверке</div>
|
||
</div>
|
||
<div class="kt-ai-kpi">
|
||
<div class="value" id="kpi-rejected" style="color: var(--kt-ai-status-risk-fg)">0</div>
|
||
<div class="label">Отклонено</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Фильтры -->
|
||
<div class="kt-ai-card" style="margin-bottom: var(--kt-ai-space-6)">
|
||
<div class="filter-bar">
|
||
<strong style="color: var(--kt-ai-fg-muted)">Фильтры:</strong>
|
||
<select class="kt-ai-select" id="filter-type">
|
||
<option value="all">Все типы</option>
|
||
<option value="add">Добавлено</option>
|
||
<option value="remove">Удалено</option>
|
||
<option value="modify">Изменено</option>
|
||
</select>
|
||
<select class="kt-ai-select" id="filter-status">
|
||
<option value="all">Все статусы</option>
|
||
<option value="pending">На проверке</option>
|
||
<option value="accepted">Принято</option>
|
||
<option value="rejected">Отклонено</option>
|
||
</select>
|
||
<select class="kt-ai-select" id="filter-period">
|
||
<option value="7d">7 дней</option>
|
||
<option value="14d">14 дней</option>
|
||
<option value="30d">30 дней</option>
|
||
</select>
|
||
<span class="kt-ai-spacer"></span>
|
||
<button class="kt-ai-btn" data-variant="ghost" id="resetFilters">Сбросить</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Основная сетка сравнения -->
|
||
<div class="comparison-grid">
|
||
<!-- Левая панель: было -->
|
||
<div class="comparison-panel">
|
||
<div class="kt-ai-card">
|
||
<h3 style="margin-bottom: var(--kt-ai-space-4)">Версия 1.2.4 (было)</h3>
|
||
<div class="screenshot-placeholder">
|
||
<div style="text-align:center">
|
||
<div style="font-size:48px;margin-bottom:var(--kt-ai-space-2)">📸</div>
|
||
<div>Скриншот старой версии</div>
|
||
<div style="font-size:var(--kt-ai-text-xs);margin-top:var(--kt-ai-space-1)">800×450</div>
|
||
</div>
|
||
</div>
|
||
<div style="margin-top: var(--kt-ai-space-4); font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted)">
|
||
<div>Дата: <strong>01.09.2026</strong></div>
|
||
<div>Автор: <strong>Иванов А.</strong></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Список изменений -->
|
||
<div class="kt-ai-card">
|
||
<h3 style="margin-bottom: var(--kt-ai-space-4)">Изменения</h3>
|
||
<div id="changes-list" style="display:flex;flex-direction:column;gap:var(--kt-ai-space-3)">
|
||
<!-- Заполняется JS -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Правая панель: стало -->
|
||
<div class="comparison-panel">
|
||
<div class="kt-ai-card">
|
||
<h3 style="margin-bottom: var(--kt-ai-space-4)">Версия 1.2.5 (стало)</h3>
|
||
<div class="screenshot-placeholder">
|
||
<div style="text-align:center">
|
||
<div style="font-size:48px;margin-bottom:var(--kt-ai-space-2)">📸</div>
|
||
<div>Скриншот новой версии</div>
|
||
<div style="font-size:var(--kt-ai-text-xs);margin-top:var(--kt-ai-space-1)">800×450</div>
|
||
</div>
|
||
</div>
|
||
<div style="margin-top: var(--kt-ai-space-4); font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted)">
|
||
<div>Дата: <strong>08.09.2026</strong></div>
|
||
<div>Автор: <strong>Серик К.</strong></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Детали выбранного изменения -->
|
||
<div class="kt-ai-card" id="detail-panel">
|
||
<h3 style="margin-bottom: var(--kt-ai-space-4)">Детали</h3>
|
||
<div id="detail-content" class="empty-state">
|
||
Выберите изменение из списка
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Комментарии -->
|
||
<div class="comment-section kt-ai-card">
|
||
<h3 style="margin-bottom: var(--kt-ai-space-4)">Комментарии к сравнению</h3>
|
||
<div style="display:flex;gap:var(--kt-ai-space-3);margin-bottom:var(--kt-ai-space-4)">
|
||
<input type="text" class="kt-ai-input" id="comment-input" placeholder="Добавить комментарий..." style="flex:1">
|
||
<button class="kt-ai-btn" data-variant="primary" id="addCommentBtn">Добавить</button>
|
||
</div>
|
||
<div class="comment-list" id="comment-list">
|
||
<!-- Комментарии JS -->
|
||
</div>
|
||
</div>
|
||
|
||
</main>
|
||
|
||
<footer class="kt-ai-wrap kt-ai-pagefoot" style="margin-top: var(--kt-ai-space-12)">
|
||
<span>© 2026 Казахтелеком AI</span>
|
||
<span class="kt-ai-spacer"></span>
|
||
<span style="color: var(--kt-ai-fg-muted)">Экран сравнения версий v1.0</span>
|
||
</footer>
|
||
|
||
<script>
|
||
// === Демо-данные ===
|
||
const DEMO_CHANGES = [
|
||
{ id: 1, type: 'add', title: 'Кнопка "Экспорт в Excel"', desc: 'Добавлена возможность экспорта таблицы в Excel', status: 'pending', date: '2026-09-08' },
|
||
{ id: 2, type: 'modify', title: 'Цвет статусов', desc: 'Изменена палитра статусов на более контрастную', status: 'pending', date: '2026-09-08' },
|
||
{ id: 3, type: 'remove', title: 'Убрана анимация загрузки', desc: 'Удалена анимация для ускорения работы', status: 'accepted', date: '2026-09-07' },
|
||
{ id: 4, type: 'add', title: 'Фильтр по периодам', desc: 'Добавлен новый фильтр "Период"', status: 'pending', date: '2026-09-08' },
|
||
{ id: 5, type: 'modify', title: 'Размер шрифта в таблице', desc: 'Увеличен с 12px до 14px для читаемости', status: 'rejected', date: '2026-09-06' },
|
||
{ id: 6, type: 'add', title: 'Иконки в меню', desc: 'Добавлены иконки к пунктам навигации', status: 'pending', date: '2026-09-08' },
|
||
{ id: 7, type: 'remove', title: 'Лишние разделители', desc: 'Убраны визуальные разделители в карточках', status: 'pending', date: '2026-09-07' },
|
||
{ id: 8, type: 'modify', title: 'Отступы в форме', desc: 'Увеличены отступы между полями', status: 'accepted', date: '2026-09-05' }
|
||
];
|
||
|
||
const DEMO_COMMENTS = [
|
||
{ id: 1, author: 'Серик К.', text: 'Просьба принять все правки до конца недели', date: '2026-09-08 10:30' },
|
||
{ id: 2, author: 'Иванов А.', text: 'По пункту 5 — не согласен, шрифт должен быть крупнее', date: '2026-09-07 14:15' },
|
||
{ id: 3, author: 'Ольга М.', text: 'Визуально стало лучше, поддерживаю изменения', date: '2026-09-06 09:00' }
|
||
];
|
||
|
||
// === Состояние ===
|
||
let changes = JSON.parse(localStorage.getItem('comparison_changes')) || DEMO_CHANGES;
|
||
let comments = JSON.parse(localStorage.getItem('comparison_comments')) || DEMO_COMMENTS;
|
||
let selectedChangeId = null;
|
||
|
||
// === Реестр фильтров ===
|
||
const FILTERS = [
|
||
{ id: 'type', label: 'Тип', affects: ['table', 'stats'] },
|
||
{ id: 'status', label: 'Статус', affects: ['table', 'stats'] },
|
||
{ id: 'period', label: 'Период', affects: ['table'], demoOnly: true }
|
||
];
|
||
|
||
// === Рендеринг ===
|
||
function renderChanges() {
|
||
const container = document.getElementById('changes-list');
|
||
const typeFilter = document.getElementById('filter-type').value;
|
||
const statusFilter = document.getElementById('filter-status').value;
|
||
|
||
const filtered = changes.filter(c => {
|
||
if (typeFilter !== 'all' && c.type !== typeFilter) return false;
|
||
if (statusFilter !== 'all' && c.status !== statusFilter) return false;
|
||
return true;
|
||
});
|
||
|
||
if (filtered.length === 0) {
|
||
container.innerHTML = '<div class="empty-state">Нет изменений по выбранным фильтрам</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = filtered.map(c => {
|
||
const iconClass = c.type === 'add' ? 'add' : c.type === 'remove' ? 'remove' : 'modify';
|
||
const iconText = c.type === 'add' ? '+' : c.type === 'remove' ? '−' : '≈';
|
||
const statusClass = c.status === 'accepted' ? 'accepted' : c.status === 'rejected' ? 'rejected' : '';
|
||
return `
|
||
<div class="change-item ${statusClass}" data-id="${c.id}" style="cursor:pointer">
|
||
<div class="change-icon ${iconClass}">${iconText}</div>
|
||
<div class="change-content">
|
||
<div class="change-title">${c.title}</div>
|
||
<div class="change-desc">${c.desc}</div>
|
||
</div>
|
||
<button class="kt-ai-btn ${c.status === 'pending' ? '' : 'disabled'}"
|
||
data-variant="${c.status === 'rejected' ? 'ghost' : 'primary'}"
|
||
data-size="sm"
|
||
onclick="event.stopPropagation(); toggleStatus(${c.id})">
|
||
${c.status === 'accepted' ? '✓' : c.status === 'rejected' ? '✗' : 'Принять'}
|
||
</button>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
// Клик для выбора
|
||
container.querySelectorAll('.change-item').forEach(el => {
|
||
el.addEventListener('click', () => {
|
||
const id = parseInt(el.dataset.id);
|
||
selectedChangeId = id;
|
||
renderDetail(id);
|
||
});
|
||
});
|
||
|
||
renderStats();
|
||
}
|
||
|
||
function renderDetail(id) {
|
||
const change = changes.find(c => c.id === id);
|
||
if (!change) return;
|
||
|
||
const container = document.getElementById('detail-content');
|
||
const typeLabels = { add: 'Добавлено', remove: 'Удалено', modify: 'Изменено' };
|
||
const statusLabels = { pending: 'На проверке', accepted: 'Принято', rejected: 'Отклонено' };
|
||
|
||
container.innerHTML = `
|
||
<div style="margin-bottom: var(--kt-ai-space-4)">
|
||
<span class="kt-ai-chip" data-tone="blue">${typeLabels[change.type]}</span>
|
||
<span class="kt-ai-chip" style="margin-left: var(--kt-ai-space-2)"
|
||
data-tone="${change.status === 'accepted' ? 'green' : change.status === 'rejected' ? 'red' : 'orange'}">
|
||
${statusLabels[change.status]}
|
||
</span>
|
||
</div>
|
||
<h4 style="margin-bottom: var(--kt-ai-space-2)">${change.title}</h4>
|
||
<p style="color: var(--kt-ai-fg-muted); margin-bottom: var(--kt-ai-space-4)">${change.desc}</p>
|
||
<div style="font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-faint)">
|
||
<div>Дата изменения: <strong>${change.date}</strong></div>
|
||
<div>Автор: <strong>Серик К.</strong></div>
|
||
</div>
|
||
${change.status === 'pending' ? `
|
||
<div style="margin-top: var(--kt-ai-space-4); display: flex; gap: var(--kt-ai-space-3)">
|
||
<button class="kt-ai-btn" data-variant="primary" onclick="toggleStatus(${change.id})">Принять</button>
|
||
<button class="kt-ai-btn" data-variant="ghost" onclick="toggleStatus(${change.id}, true)">Отклонить</button>
|
||
</div>
|
||
` : ''}
|
||
`;
|
||
}
|
||
|
||
function renderStats() {
|
||
const total = changes.length;
|
||
const accepted = changes.filter(c => c.status === 'accepted').length;
|
||
const rejected = changes.filter(c => c.status === 'rejected').length;
|
||
const pending = changes.filter(c => c.status === 'pending').length;
|
||
|
||
document.getElementById('kpi-total').textContent = total;
|
||
document.getElementById('kpi-accepted').textContent = accepted;
|
||
document.getElementById('kpi-pending').textContent = pending;
|
||
document.getElementById('kpi-rejected').textContent = rejected;
|
||
}
|
||
|
||
function renderComments() {
|
||
const container = document.getElementById('comment-list');
|
||
if (comments.length === 0) {
|
||
container.innerHTML = '<div class="empty-state">Пока нет комментариев</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = comments.map(c => `
|
||
<div class="comment-item">
|
||
<div class="comment-meta">
|
||
<strong>${c.author}</strong> · ${c.date}
|
||
</div>
|
||
<div style="color: var(--kt-ai-fg)">${c.text}</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
// === Действия ===
|
||
function toggleStatus(id, reject = false) {
|
||
const change = changes.find(c => c.id === id);
|
||
if (!change || change.status !== 'pending') return;
|
||
|
||
change.status = reject ? 'rejected' : 'accepted';
|
||
localStorage.setItem('comparison_changes', JSON.stringify(changes));
|
||
|
||
renderChanges();
|
||
if (selectedChangeId === id) renderDetail(id);
|
||
}
|
||
|
||
function approveAll() {
|
||
let count = 0;
|
||
changes.forEach(c => {
|
||
if (c.status === 'pending') {
|
||
c.status = 'accepted';
|
||
count++;
|
||
}
|
||
});
|
||
localStorage.setItem('comparison_changes', JSON.stringify(changes));
|
||
renderChanges();
|
||
renderDetail(selectedChangeId);
|
||
alert(`Принято ${count} изменений`);
|
||
}
|
||
|
||
function addComment() {
|
||
const input = document.getElementById('comment-input');
|
||
const text = input.value.trim();
|
||
if (!text) return;
|
||
|
||
const newComment = {
|
||
id: Date.now(),
|
||
author: 'Вы',
|
||
text: text,
|
||
date: new Date().toLocaleString('ru-RU', {
|
||
day: '2-digit', month: '2-digit', year: 'numeric',
|
||
hour: '2-digit', minute: '2-digit'
|
||
})
|
||
};
|
||
|
||
comments.unshift(newComment);
|
||
localStorage.setItem('comparison_comments', JSON.stringify(comments));
|
||
input.value = '';
|
||
renderComments();
|
||
}
|
||
|
||
function resetFilters() {
|
||
document.getElementById('filter-type').value = 'all';
|
||
document.getElementById('filter-status').value = 'all';
|
||
document.getElementById('filter-period').value = '7d';
|
||
renderChanges();
|
||
}
|
||
|
||
// === Переключение темы ===
|
||
function toggleTheme() {
|
||
const html = document.documentElement;
|
||
const current = html.getAttribute('data-theme');
|
||
const next = current === 'light' ? 'dark' : 'light';
|
||
html.setAttribute('data-theme', next);
|
||
localStorage.setItem('comparison_theme', next);
|
||
}
|
||
|
||
// === Инициализация ===
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
// Тема
|
||
const savedTheme = localStorage.getItem('comparison_theme') || 'light';
|
||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||
document.getElementById('themeToggle').addEventListener('click', toggleTheme);
|
||
|
||
// Кнопки
|
||
document.getElementById('approveAllBtn').addEventListener('click', approveAll);
|
||
document.getElementById('addCommentBtn').addEventListener('click', addComment);
|
||
document.getElementById('resetFilters').addEventListener('click', resetFilters);
|
||
|
||
// Фильтры
|
||
['filter-type', 'filter-status', 'filter-period'].forEach(id => {
|
||
document.getElementById(id).addEventListener('change', renderChanges);
|
||
});
|
||
|
||
// Enter в комментарии
|
||
document.getElementById('comment-input').addEventListener('keypress', (e) => {
|
||
if (e.key === 'Enter') addComment();
|
||
});
|
||
|
||
renderChanges();
|
||
renderComments();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|