today dashboard: activity_events, ListTodayView with events timeline, frontend TodayDashboard separated from sidebar

This commit is contained in:
2026-06-01 02:16:13 +08:00
parent 69891e395c
commit c74fa3ad43
27 changed files with 665 additions and 98 deletions
+146 -2
View File
@@ -26,6 +26,7 @@
// ===== State =====
let sections = []
let nodes = []
let todayDashboard = null
let version = ''
let error = ''
let selectedSection = ''
@@ -134,15 +135,18 @@
worklog = []
showCreateNode = false
error = ''
todayDashboard = null
nodes = []
try {
if (id === 'today') {
nodes = await wailsCall('ListTodayView') || []
todayDashboard = await wailsCall('ListTodayView') || { cases: [] }
} else {
nodes = await wailsCall('ListNodesBySection', id) || []
}
} catch (e) {
error = String(e)
nodes = []
todayDashboard = { cases: [] }
}
}
@@ -755,10 +759,59 @@
// ===== Helpers =====
function tabClass(id) { return activeTab === id ? 'tab active' : 'tab' }
function eventLabel(type) {
const labels = {
'note_created': 'Заметка создана',
'note_updated': 'Заметка изменена',
'file_added': 'Файл добавлен',
'file_deleted': 'Файл удалён',
'file_renamed': 'Файл переименован',
'file_copied': 'Файл скопирован',
'file_moved': 'Файл перемещён',
'folder_added': 'Папка добавлена',
'folder_deleted': 'Папка удалена',
'folder_renamed': 'Папка переименована',
'node_created': 'Дело создано',
'node_updated': 'Дело изменено',
}
return labels[type] || type
}
function eventIcon(type) {
if (type === 'note_created' || type === 'file_added' || type === 'folder_added' || type === 'node_created') return '+'
if (type === 'file_deleted' || type === 'folder_deleted') return '×'
if (type === 'file_renamed' || type === 'folder_renamed' || type === 'note_updated' || type === 'node_updated') return '~'
if (type === 'file_copied') return '⧉'
if (type === 'file_moved') return '→'
return '•'
}
function formatTime(str) {
if (!str) return ''
try { return new Date(str).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }) } catch (e) { return '' }
}
function formatDate(str) {
if (!str) return ''
try { return new Date(str).toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' }) } catch (e) { return str }
}
function nodeKindLabel(kind) {
const labels = { 'project': 'Проект', 'client': 'Клиент', 'document': 'Документ', 'recipe': 'Рецепт', 'archive': 'Архив', 'case': 'Дело' }
return labels[kind] || kind || 'Дело'
}
function pluralize(n, one, few, many) {
n = Math.abs(n) % 100
if (n >= 5 && n <= 20) return many
n %= 10
if (n === 1) return one
if (n >= 2 && n <= 4) return few
return many
}
async function openNodeById(id) {
try {
const node = await wailsCall('GetNodeDetail', id)
if (node) selectNode(node)
} catch (e) {
error = String(e)
}
}
</script>
<div class="app">
@@ -778,7 +831,7 @@
</button>
{/each}
</div>
{#if selectedSection}
{#if selectedSection && selectedSection !== 'today' && selectedSection !== 'inbox'}
<div class="nav-group">
<div class="nav-label">Дела {#if nodes.length > 0}({nodes.length}){/if}</div>
{#each nodes as node}
@@ -1048,6 +1101,66 @@
{/if}
</div>
{:else if selectedSection === 'today' && todayDashboard}
<div class="today-dashboard">
<div class="today-header">
<h2>Сегодня</h2>
<span class="today-date">{todayDashboard.date}</span>
</div>
{#if todayDashboard.summary}
<div class="today-summary">
{#if todayDashboard.summary.changedCases > 0}<span class="summary-chip">{todayDashboard.summary.changedCases} {pluralize(todayDashboard.summary.changedCases, 'дело', 'дела', 'дел')}</span>{/if}
{#if todayDashboard.summary.notes > 0}<span class="summary-chip">{todayDashboard.summary.notes} {pluralize(todayDashboard.summary.notes, 'заметка', 'заметки', 'заметок')}</span>{/if}
{#if todayDashboard.summary.files > 0}<span class="summary-chip">{todayDashboard.summary.files} {pluralize(todayDashboard.summary.files, 'файл', 'файла', 'файлов')}</span>{/if}
</div>
{/if}
{#if todayDashboard.groups && todayDashboard.groups.length > 0}
{#each todayDashboard.groups as group}
<div class="today-case">
<div class="today-case-header" role="button" tabindex="0" on:click={() => openNodeById(group.nodeId)} on:keydown={(e) => e.key === 'Enter' && openNodeById(group.nodeId)}>
<span class="today-case-title">{group.nodeTitle}</span>
<span class="today-case-type">{nodeKindLabel(group.nodeKind)}</span>
<span class="today-case-time">{formatTime(group.lastActivityAt)}</span>
</div>
{#if group.events && group.events.length > 0}
<div class="today-events">
{#each group.events as ev}
<div class="today-event">
<span class="today-event-icon">{eventIcon(ev.eventType)}</span>
<span class="today-event-title">{ev.title}</span>
<span class="today-event-type">{eventLabel(ev.eventType)}</span>
<span class="today-event-time">{formatTime(ev.createdAt)}</span>
</div>
{/each}
</div>
{:else}
<div class="today-events-empty">Изменён сегодня, подробная история пока недоступна</div>
{/if}
</div>
{/each}
{#if todayDashboard.events && todayDashboard.events.length > 0}
<div class="today-timeline">
<h3>Лента за сегодня</h3>
{#each todayDashboard.events as ev}
<div class="timeline-event">
<span class="timeline-dot"></span>
<span class="timeline-title">{ev.title}</span>
<span class="timeline-type">{eventLabel(ev.eventType)}</span>
<span class="timeline-time">{formatTime(ev.createdAt)}</span>
</div>
{/each}
</div>
{/if}
{:else}
<div class="today-empty">
<p>Сегодня пока тихо</p>
<p class="hint">Здесь появятся дела, заметки, файлы и действия, с которыми вы работали сегодня.</p>
</div>
{/if}
</div>
{:else}
<div class="welcome">
<h2>Верстак</h2>
@@ -1286,4 +1399,35 @@
.summary-warn { margin-top: 8px; padding: 8px 12px; background: #3a2a22; border-radius: 6px; color: #ffaa66; font-size: 13px; }
.rename-error { color: #ff6b6b; font-size: 12px; margin-top: 4px; }
/* Today Dashboard */
.today-dashboard { padding: 24px; overflow-y: auto; flex: 1; }
.today-header { display: flex; align-items: baseline; gap: 12px; margin-bottom: 16px; }
.today-header h2 { font-size: 24px; }
.today-date { font-size: 13px; color: #666; }
.today-summary { display: flex; gap: 8px; margin-bottom: 20px; flex-wrap: wrap; }
.summary-chip { font-size: 12px; color: #b0b0c0; background: #1a1a28; border: 1px solid #2a2a3c; padding: 4px 12px; border-radius: 16px; }
.today-case { background: #1a1a28; border: 1px solid #2a2a3c; border-radius: 8px; margin-bottom: 12px; overflow: hidden; }
.today-case-header { padding: 12px 16px; display: flex; align-items: center; gap: 8px; border-bottom: 1px solid #2a2a3c; cursor: pointer; }
.today-case-header:hover { background: #1e1e30; }
.today-case-title { font-weight: 500; }
.today-case-type { font-size: 11px; color: #888; background: #222233; padding: 2px 8px; border-radius: 10px; }
.today-case-time { font-size: 11px; color: #555; margin-left: auto; }
.today-events { padding: 8px 16px; }
.today-event { display: flex; align-items: center; gap: 8px; padding: 4px 0; font-size: 13px; color: #b0b0c0; }
.today-event-icon { width: 18px; text-align: center; color: #6366f1; font-size: 13px; }
.today-event-title { flex: 1; }
.today-event-type { font-size: 11px; color: #666; }
.today-event-time { font-size: 11px; color: #555; margin-left: auto; }
.today-events-empty { padding: 8px 16px; font-size: 13px; color: #666; font-style: italic; }
.today-empty { padding: 48px 24px; text-align: center; }
.today-empty p { color: #666; font-size: 14px; margin: 0; }
.today-empty .hint { font-size: 12px; color: #555; margin-top: 8px; }
.today-timeline { margin-top: 24px; }
.today-timeline h3 { font-size: 13px; color: #666; text-transform: uppercase; margin-bottom: 12px; }
.timeline-event { display: flex; align-items: center; gap: 10px; padding: 6px 0; font-size: 13px; color: #b0b0c0; border-left: 2px solid #2a2a3c; padding-left: 16px; margin-left: 4px; }
.timeline-dot { width: 6px; height: 6px; border-radius: 50%; background: #6366f1; margin-left: -19px; flex-shrink: 0; }
.timeline-title { flex: 1; }
.timeline-type { font-size: 11px; color: #666; }
.timeline-time { font-size: 11px; color: #555; }
</style>