feat: плагин-система Lua + Calendar reference plugin
- Lua VM runtime: gopher-lua с песочницей, хуки on_init/on_tick/on_shutdown - API: verstak.node.* / verstak.db.* / verstak.config.* / verstak.state.* - API: verstak.worklog.* / verstak.activity.* / verstak.file.* - API: verstak.schedule.* / verstak.http.* / verstak.ui.* - Менеджер плагинов: жизненный цикл, инициализация, шаблоны - Scheduler: фоновые задачи с интервалами - PluginPage.svelte: контейнер для iframe-панелей плагинов - Calendar plugin: миграция, категории CRUD, события CRUD - Calendar: расширенный рекарренс (daily/weekly/monthly/yearly) - Calendar: связь с узлами Верстака, напоминания, HTTP-праздники - Calendar: Lua-тест-сьют (15 тестов), Go-интеграционный тест - fix: query_row использует реальные Column() вместо guessColumns
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Календарь</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #1a1b1e; color: #e4e4e7; padding: 20px; }
|
||||
h1 { font-size: 24px; margin-bottom: 16px; color: #fff; }
|
||||
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.nav-btn { background: #27272a; border: 1px solid #3f3f46; color: #e4e4e7;
|
||||
padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 14px; }
|
||||
.nav-btn:hover { background: #3f3f46; }
|
||||
.calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
|
||||
.day-header { text-align: center; font-size: 12px; color: #a1a1aa; padding: 8px 0;
|
||||
font-weight: 600; text-transform: uppercase; }
|
||||
.day { aspect-ratio: 1; display: flex; flex-direction: column; align-items: center;
|
||||
justify-content: center; border-radius: 8px; cursor: pointer; font-size: 14px;
|
||||
background: #27272a; border: 1px solid #3f3f46; position: relative; }
|
||||
.day:hover { background: #3f3f46; }
|
||||
.day.other { opacity: 0.3; }
|
||||
.day.today { border-color: #3b82f6; background: #1e3a5f; }
|
||||
.day.has-event::after { content: ''; position: absolute; bottom: 6px; width: 6px; height: 6px;
|
||||
border-radius: 50%; background: #3b82f6; }
|
||||
.day.selected { border-color: #8b5cf6; box-shadow: 0 0 0 2px #8b5cf6; }
|
||||
.events { margin-top: 20px; }
|
||||
.event { padding: 12px; background: #27272a; border: 1px solid #3f3f46;
|
||||
border-radius: 8px; margin-bottom: 8px; display: flex; justify-content: space-between; }
|
||||
.event-title { font-weight: 500; }
|
||||
.event-time { color: #a1a1aa; font-size: 13px; }
|
||||
.event-actions { display: flex; gap: 8px; }
|
||||
.event-actions button { background: none; border: none; color: #a1a1aa;
|
||||
cursor: pointer; padding: 2px 6px; border-radius: 4px; }
|
||||
.event-actions button:hover { background: #3f3f46; color: #ef4444; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<button class="nav-btn" onclick="prevMonth()">←</button>
|
||||
<h1 id="month-title">Июнь 2025</h1>
|
||||
<button class="nav-btn" onclick="nextMonth()">→</button>
|
||||
</div>
|
||||
<div class="calendar-grid" id="calendar"></div>
|
||||
<div class="events" id="events"></div>
|
||||
|
||||
<script>
|
||||
const MONTHS = ['Январь','Февраль','Март','Апрель','Май','Июнь',
|
||||
'Июль','Август','Сентябрь','Октябрь','Ноябрь','Декабрь'];
|
||||
const DAYS = ['Пн','Вт','Ср','Чт','Пт','Сб','Вс'];
|
||||
|
||||
let currentDate = new Date();
|
||||
let selectedDate = null;
|
||||
let events = [];
|
||||
|
||||
function render() {
|
||||
const year = currentDate.getFullYear();
|
||||
const month = currentDate.getMonth();
|
||||
document.getElementById('month-title').textContent = MONTHS[month] + ' ' + year;
|
||||
|
||||
const firstDay = new Date(year, month, 1);
|
||||
const lastDay = new Date(year, month + 1, 0);
|
||||
const startDay = firstDay.getDay() || 7; // Mon=1, Sun=7
|
||||
|
||||
const grid = document.getElementById('calendar');
|
||||
grid.innerHTML = DAYS.map(d => `<div class="day-header">${d}</div>`).join('');
|
||||
|
||||
// Previous month padding
|
||||
const prevLastDay = new Date(year, month, 0).getDate();
|
||||
for (let i = startDay - 2; i >= 0; i--) {
|
||||
grid.innerHTML += `<div class="day other">${prevLastDay - i}</div>`;
|
||||
}
|
||||
|
||||
// Current month
|
||||
const today = new Date();
|
||||
for (let d = 1; d <= lastDay.getDate(); d++) {
|
||||
const dateStr = `${year}-${String(month+1).padStart(2,'0')}-${String(d).padStart(2,'0')}`;
|
||||
const isToday = today.getFullYear() === year && today.getMonth() === month && today.getDate() === d;
|
||||
const hasEvent = events.some(e => e.date === dateStr);
|
||||
const isSelected = selectedDate === dateStr;
|
||||
let cls = 'day';
|
||||
if (isToday) cls += ' today';
|
||||
if (hasEvent) cls += ' has-event';
|
||||
if (isSelected) cls += ' selected';
|
||||
grid.innerHTML += `<div class="${cls}" onclick="selectDate('${dateStr}')">${d}</div>`;
|
||||
}
|
||||
|
||||
renderEvents();
|
||||
}
|
||||
|
||||
function renderEvents() {
|
||||
const el = document.getElementById('events');
|
||||
if (!selectedDate) { el.innerHTML = '<p style="color:#a1a1aa">Выберите дату</p>'; return; }
|
||||
|
||||
const dayEvents = events.filter(e => e.date === selectedDate);
|
||||
if (dayEvents.length === 0) {
|
||||
el.innerHTML = '<p style="color:#a1a1aa">Нет событий на этот день</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
el.innerHTML = '<h2 style="font-size:18px;margin-bottom:12px">События</h2>';
|
||||
dayEvents.forEach(ev => {
|
||||
const time = ev.time || 'весь день';
|
||||
el.innerHTML += `
|
||||
<div class="event">
|
||||
<div>
|
||||
<div class="event-title">${ev.title}</div>
|
||||
<div class="event-time">${time}${ev.location ? ' · ' + ev.location : ''}</div>
|
||||
</div>
|
||||
<div class="event-actions">
|
||||
<button onclick="deleteEvent('${ev.id}')">✕</button>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
function selectDate(dateStr) {
|
||||
selectedDate = dateStr;
|
||||
render();
|
||||
}
|
||||
|
||||
function prevMonth() {
|
||||
currentDate.setMonth(currentDate.getMonth() - 1);
|
||||
selectedDate = null;
|
||||
render();
|
||||
}
|
||||
|
||||
function nextMonth() {
|
||||
currentDate.setMonth(currentDate.getMonth() + 1);
|
||||
selectedDate = null;
|
||||
render();
|
||||
}
|
||||
|
||||
async function loadEvents() {
|
||||
// In production, this would call verstak HTTP endpoint or Wails binding
|
||||
// For now, data is injected via __VERSTAK_DATA__
|
||||
events = window.__VERSTAK_DATA__ || [];
|
||||
render();
|
||||
}
|
||||
|
||||
loadEvents();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user